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

249 lines
6.8 KiB
TypeScript

/**
* HTTP请求工具
* 基于axios封装的统一请求实例
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { envConfig } from '@/config/env'
// 请求响应接口
export interface ApiResponse<T = any> {
code: number
message: string
data: T
success: boolean
timestamp: number
}
// 请求配置接口
export interface RequestConfig extends AxiosRequestConfig {
// 是否显示loading
showLoading?: boolean
// 是否显示错误消息
showError?: boolean
// 是否需要token
needToken?: boolean
}
// 创建axios实例
const createAxiosInstance = (): AxiosInstance => {
const instance = axios.create({
baseURL: envConfig.apiBaseUrl,
timeout: 30000,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 请求拦截器
instance.interceptors.request.use(
(config: any) => {
// 添加token
const token = localStorage.getItem('access_token')
console.log('🔑 请求拦截器 - Token状态:', {
hasToken: !!token,
tokenPreview: token ? `${token.substring(0, 20)}...` : 'null',
url: config.url,
needToken: config.needToken
})
if (token && config.needToken !== false) {
config.headers.Authorization = `Bearer ${token}`
console.log('🔑 已添加Authorization头')
} else {
console.log('🔑 未添加Authorization头 - 原因:', !token ? '无token' : 'needToken=false')
}
// 添加请求ID用于追踪
config.headers['X-Request-ID'] = generateRequestId()
// 打印请求日志
if (envConfig.debug) {
console.log('🚀 Request:', {
url: config.url,
method: config.method,
params: config.params,
data: config.data,
headers: config.headers
})
}
return config
},
(error: AxiosError) => {
console.error('❌ Request Error:', error)
return Promise.reject(error)
}
)
// 响应拦截器
instance.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
const { data } = response
// 打印响应日志
if (envConfig.debug) {
console.log('✅ Response:', {
url: response.config.url,
status: response.status,
data: data
})
}
// 检查业务状态码
if (data.code === 200 || data.success) {
return data
}
// 处理业务错误
const errorMessage = data.message || '请求失败'
// 特殊错误码处理
switch (data.code) {
case 401:
console.warn('🚫 业务层401错误:', errorMessage)
// 只有在非登录接口时才处理401
if (!response.config.url?.includes('/auth/login')) {
handleUnauthorized()
} else {
ElMessage.error(errorMessage)
}
break
case 403:
ElMessage.error('没有权限访问该资源')
break
case 404:
ElMessage.error('请求的资源不存在')
break
case 500:
ElMessage.error('服务器内部错误')
break
default:
ElMessage.error(errorMessage)
}
return Promise.reject(new Error(errorMessage))
},
(error: AxiosError) => {
console.error('❌ Response Error:', error)
let errorMessage = '网络请求失败'
if (error.response) {
// 服务器响应错误
const { status, data } = error.response
switch (status) {
case 400:
errorMessage = '请求参数错误'
break
case 401:
errorMessage = '未授权,请重新登录'
console.warn('🚫 HTTP层401错误')
// 只有在非登录接口时才处理401
if (!error.config?.url?.includes('/auth/login')) {
handleUnauthorized()
}
break
case 403:
errorMessage = '拒绝访问'
break
case 404:
errorMessage = '请求地址不存在'
break
case 408:
errorMessage = '请求超时'
break
case 500:
errorMessage = '服务器内部错误'
break
case 502:
errorMessage = '网关错误'
break
case 503:
errorMessage = '服务不可用'
break
case 504:
errorMessage = '网关超时'
break
default:
errorMessage = (data as any)?.message || `请求失败 (${status})`
}
} else if (error.request) {
// 网络错误
errorMessage = '网络连接失败,请检查网络'
} else {
// 其他错误
errorMessage = error.message || '请求配置错误'
}
ElMessage.error(errorMessage)
return Promise.reject(error)
}
)
return instance
}
// 处理未授权
const handleUnauthorized = () => {
console.warn('🚫 收到401未授权响应')
// 检查当前页面是否是登录页,避免在登录页面重复处理
if (window.location.pathname === '/login') {
console.log('🚫 当前在登录页面,不处理401错误')
return
}
// 不立即清除认证信息,而是提示用户
ElMessageBox.confirm(
'登录状态已过期,请重新登录',
'提示',
{
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
// 用户确认后才清除认证信息
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
localStorage.removeItem('user_info')
// 跳转到登录页
window.location.href = '/login'
}).catch(() => {
// 用户取消,不清除认证信息,让用户继续操作
console.log('🚫 用户取消重新登录')
})
}
// 生成请求ID
const generateRequestId = (): string => {
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
}
// 创建请求实例
export const request = createAxiosInstance()
// 导出请求方法
export const http = {
get: <T = any>(url: string, config?: RequestConfig) =>
request.get<any, ApiResponse<T>>(url, config),
post: <T = any>(url: string, data?: any, config?: RequestConfig) =>
request.post<any, ApiResponse<T>>(url, data, config),
put: <T = any>(url: string, data?: any, config?: RequestConfig) =>
request.put<any, ApiResponse<T>>(url, data, config),
delete: <T = any>(url: string, config?: RequestConfig) =>
request.delete<any, ApiResponse<T>>(url, config),
patch: <T = any>(url: string, data?: any, config?: RequestConfig) =>
request.patch<any, ApiResponse<T>>(url, data, config)
}
export default request