Files
pqs-9100_client/frontend/src/api/index.ts
2025-11-26 08:50:22 +08:00

243 lines
10 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.

import { ElMessage } from 'element-plus'
import axios, {
AxiosError,
type AxiosInstance,
type AxiosRequestConfig,
type AxiosResponse,
type InternalAxiosRequestConfig
} from 'axios'
import { showFullScreenLoading, tryHideFullScreenLoading } from '@/components/Loading/fullScreen'
import { LOGIN_URL } from '@/config'
import { type ResultData } from '@/api/interface'
import { ResultEnum } from '@/enums/httpEnum'
import { checkStatus } from './helper/checkStatus'
import { useUserStore } from '@/stores/modules/user'
import router from '@/routers'
import { refreshToken } from '@/api/user/login'
import { EventSourcePolyfill } from 'event-source-polyfill'
export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
loading?: boolean
}
const config = {
// 默认地址请求地址,可在 .env 开头文件中修改
baseURL: import.meta.env.VITE_API_URL as string,
// 设置超时时间60s
timeout: 60000,
// 跨域时候允许携带凭证
withCredentials: true,
// post请求指定数据类型以及编码
headers: { 'Content-Type': 'application/json;charset=utf-8' }
}
class RequestHttp {
service: AxiosInstance
public constructor(config: AxiosRequestConfig) {
// 创建实例
this.service = axios.create(config)
/**
* @description 请求拦截器
* 客户端发送请求 -> [请求拦截器] -> 服务器
* token校验(JWT) : 接受服务器返回的 token,存储到 vuex/pinia/本地储存当中
*/
this.service.interceptors.request.use(
(config: CustomAxiosRequestConfig) => {
isFirst = true
const userStore = useUserStore()
// 当前请求不需要显示 loading在 api 服务中通过指定的第三个参数: { loading: false } 来控制
config.loading ?? (config.loading = true)
config.loading && showFullScreenLoading()
if (config.headers && typeof config.headers.set === 'function') {
config.headers.set('Authorization', 'Bearer ' + userStore.accessToken)
config.headers.set('Is-Refresh-Token', userStore.isRefreshToken + '')
}
return config
},
(error: AxiosError) => {
return Promise.reject(error)
}
)
let isFirst = true
/**
* @description 响应拦截器
* 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
*/
this.service.interceptors.response.use(
async (response: AxiosResponse) => {
const { data } = response
const userStore = useUserStore()
tryHideFullScreenLoading()
if (data.code === ResultEnum.ACCESSTOKEN_EXPIRED) {
// 用长token去换短token
userStore.setAccessToken(userStore.refreshToken)
userStore.setIsRefreshToken(true)
const result = await refreshToken()
if (result) {
//获取新token成功的话
// 有新的token后重新请求
userStore.setAccessToken(result.data.accessToken)
userStore.setRefreshToken(result.data.refreshToken)
userStore.setIsRefreshToken(false)
userStore.setExp(1000 * 60 * 60 * 24 * 30)
response.config.headers.Authorization = `Bearer ${result.data.accessToken}` //重新请求前需要将更新后的新token更换掉之前无效的token,不然会死循环
const resp = await this.service.request(response.config)
return resp
} else {
// 刷新失效,跳转登录页
}
}
// 登陆失效
if (data.code === ResultEnum.OVERDUE) {
//console.log('登陆失效')
userStore.setAccessToken('')
userStore.setRefreshToken('')
userStore.setIsRefreshToken(false)
userStore.setUserInfo({ id: '', name: '' })
userStore.setExp(0)
await router.replace(LOGIN_URL)
if (isFirst) {
//临时处理token失效弹窗多次
ElMessage.error(data.message)
isFirst = false
}
return Promise.reject(data)
}
// 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错)
if (data.code && data.code !== ResultEnum.SUCCESS) {
if (data.message.includes('&')) {
let formattedMessage = data.message.split('&').join('<br>')
if (data.message.includes(':')) {
formattedMessage = formattedMessage.replace(':', '')
}
ElMessage.error({ message: formattedMessage, dangerouslyUseHTMLString: true })
return Promise.reject(data)
}
ElMessage.error(data.message)
return Promise.reject(data)
}
// 成功请求(在页面上除非特殊情况,否则不用处理失败逻辑)
if (userStore.exp <= Date.now() && userStore.exp !== 0) {
userStore.setAccessToken('')
userStore.setRefreshToken('')
userStore.setIsRefreshToken(false)
userStore.setUserInfo({ id: '', name: '' })
userStore.setExp(0)
ElMessage.error('登录已过期,请重新登录!')
await router.replace(LOGIN_URL)
return Promise.reject(data)
}
// 对于blob类型的响应返回完整的response对象以保留响应头
if (response.config.responseType === 'blob') {
return response
}
return data
},
async (error: AxiosError) => {
const { response } = error
tryHideFullScreenLoading()
//console.log('error', error.message)
// 请求超时 && 网络错误单独判断,没有 response
if (error.message.indexOf('timeout') !== -1) ElMessage.error('请求超时!请您稍后重试')
if (error.message.indexOf('Network Error') !== -1) ElMessage.error('网络错误!请您稍后重试')
// 根据服务器响应的错误状态码,做不同的处理
if (response) checkStatus(response.status)
// 服务器结果都没有返回(可能服务器错误可能客户端断网),断网处理:可以跳转到断网页面
if (!window.navigator.onLine) router.replace('/500')
return Promise.reject(error)
}
)
}
/**
* @description 常用请求方法封装
*/
get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
return this.service.get(url, { params, ..._object })
}
post<T>(url: string, params?: object | string, _object = {}): Promise<ResultData<T>> {
return this.service.post(url, params, _object)
}
put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
return this.service.put(url, params, _object)
}
delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {
return this.service.delete(url, { params, ..._object })
}
download(url: string, params?: object, _object = {}): Promise<BlobPart> {
return this.service.post(url, params, { ..._object, responseType: 'blob' }).then(res => res.data)
}
downloadWithHeaders(url: string, params?: object, _object = {}): Promise<AxiosResponse<Blob>> {
return this.service.post(url, params, { ..._object, responseType: 'blob' })
}
upload(url: string, params?: object, _object = {}): Promise<BlobPart> {
return this.service.post(url, params, {
..._object,
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/**
* 针对excel的上传默认返回的是blob类型Excel没问题时返回json特殊处理
*/
uploadExcel(url: string, params?: object, _object = {}): Promise<BlobPart> {
return this.service
.post(url, params, {
..._object,
headers: { 'Content-Type': 'multipart/form-data' },
responseType: 'blob'
})
.then(res => res.data)
}
// 添加SSE连接方法
sse(url: string, params?: any): EventSource {
const userStore = useUserStore()
// 构造带参数的URL
let requestUrl = config.baseURL + url
if (params) {
const searchParams = new URLSearchParams()
for (const key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
searchParams.append(key, String(params[key]))
}
}
requestUrl += '?' + searchParams.toString()
}
// 创建EventSource连接
const eventSource = new EventSourcePolyfill(requestUrl, {
headers: {
Authorization: 'Bearer ' + userStore.accessToken
},
// 增加超时时间到1200秒
heartbeatTimeout: 1200000
})
// 设置默认的Authorization头部
eventSource.addEventListener('open', function () {
//console.log('SSE连接已建立')
})
// 添加错误处理
eventSource.addEventListener('error', function (err) {
console.error('SSE连接错误:', err)
})
return eventSource
}
}
export default new RequestHttp(config)