import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from 'axios' import { showFullScreenLoading, tryHideFullScreenLoading } from '@/components/Loading/fullScreen' import { LOGIN_URL } from '@/config' import { ElMessage, ElMessageBox } from 'element-plus' import { ResultData } from '@/api/interface' import { ResultEnum } from '@/enums/httpEnum' import { checkStatus } from './helper/checkStatus' import { AxiosCanceler } from './helper/axiosCancel' import { useUserStore } from '@/stores/modules/user' import router from '@/routers' import { getToken } from '@/utils/token' import { tansParams } from '@/utils/common' import { nanoid } from 'nanoid' import cache from '@/plugins/cache' import { encryptBase64, encryptWithAes, generateAesKey, decryptWithAes, decryptBase64 } from '@/utils/crypto' import { encrypt, decrypt } from '@/utils/jsEncrypt' export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig { loading?: boolean cancel?: boolean isEncrypt?: boolean } const config = { // 默认地址请求地址,可在 .env.** 文件中修改 baseURL: import.meta.env.VITE_API_URL as string, // 设置超时时间 timeout: ResultEnum.TIMEOUT as number, // 跨域时候允许携带凭证 withCredentials: true } const encryptHeader = 'encrypt-key' // 是否显示重新登录 export let isReLogin = { show: false } // export const globalHeaders = () => { // return { // Authorization: 'Bearer ' + getToken(), // clientid: import.meta.env.VITE_APP_CLIENT_ID // } // } axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' axios.defaults.headers['clientid'] = import.meta.env.VITE_APP_CLIENT_ID const axiosCanceler = new AxiosCanceler() class RequestHttp { service: AxiosInstance public constructor(config: AxiosRequestConfig) { // instantiation this.service = axios.create(config) /** * @description 请求拦截器 * 客户端发送请求 -> [请求拦截器] -> 服务器 * token校验(JWT) : 接受服务器返回的 token,存储到 vuex/pinia/本地储存当中 */ this.service.interceptors.request.use( (config: CustomAxiosRequestConfig) => { // 是否需要加密 const isEncrypt = config.isEncrypt ?? (config.isEncrypt = false) // 重复请求不需要取消,在 api 服务中通过指定的第三个参数: { cancel: false } 来控制 config.cancel ?? (config.cancel = true) config.cancel && axiosCanceler.addPending(config) // 当前请求不需要显示 loading,在 api 服务中通过指定的第三个参数: { loading: false } 来控制 config.loading ?? (config.loading = true) config.loading && showFullScreenLoading() // 是否需要设置 token const isToken = (config.headers || {}).isToken === false // 是否需要防止数据重复提交 const isRepeatSubmit = (config.headers || {}).repeatSubmit === false const requestId = nanoid() const userStore = useUserStore() if (getToken() && !isToken) { // 请求加上随机ID config.headers.set('RequestId', requestId) if (config.headers && typeof config.headers.set === 'function') { config.headers.set('Authorization', 'Bearer ' + userStore.token) } } // get请求映射params参数 if (config.method === 'get' && config.params) { let url = config.url + '?' + tansParams(config.params) url = url.slice(0, -1) config.params = {} config.url = url } if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) { const requestObj = { url: config.url, data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, time: new Date().getTime() } const sessionObj = cache.session.getJSON('sessionObj') if (sessionObj === undefined || sessionObj === null || sessionObj === '') { cache.session.setJSON('sessionObj', requestObj) } else { const s_url = sessionObj.url // 请求地址 const s_data = sessionObj.data // 请求数据 const s_time = sessionObj.time // 请求时间 const interval = 1000 // 间隔时间(ms),小于此时间视为重复提交 if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { const message = '数据正在处理,请勿重复提交' console.warn(`[${s_url}]: ` + message) return Promise.reject(new Error(message)) } else { cache.session.setJSON('sessionObj', requestObj) } } } // 当开启参数加密 if (isEncrypt && (config.method === 'post' || config.method === 'put')) { // 生成一个 AES 密钥 const aesKey = generateAesKey() config.headers[encryptHeader] = encrypt(encryptBase64(aesKey)) config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey) } // FormData数据去请求头Content-Type if (config.data instanceof FormData) { delete config.headers['Content-Type'] } return config }, (error: AxiosError) => { console.log(error) return Promise.reject(error) } ) /** * @description 响应拦截器 * 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息 */ this.service.interceptors.response.use( (response: AxiosResponse) => { // 加密后的 AES 秘钥 const keyStr = response.headers[encryptHeader] // 加密 if (keyStr != null && keyStr != '') { const data = response.data // 请求体 AES 解密 const base64Str = decrypt(keyStr) // base64 解码 得到请求头的 AES 秘钥 const aesKey = decryptBase64(base64Str.toString()) // aesKey 解码 data const decryptData = decryptWithAes(data, aesKey) // 将结果 (得到的是 JSON 字符串) 转为 JSON response.data = JSON.parse(decryptData) } const { data, config, request } = response axiosCanceler.removePending(config) tryHideFullScreenLoading() if (request.responseType === 'blob' || request.responseType === 'arraybuffer') { return data } // 登录失效 if (data.code == ResultEnum.OVERDUE) { const userStore = useUserStore() if (!isReLogin.show) { isReLogin.show = true ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }) .then(() => { isReLogin.show = false userStore.logOut().then(() => { router.replace(LOGIN_URL) }) }) .catch(() => { isReLogin.show = false }) } return Promise.reject('无效的会话,或者会话已过期,请重新登录。') } // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错) if (data.code && data.code == ResultEnum.ERROR) { ElMessage.error(data.msg) return Promise.reject(data) } // 成功请求(在页面上除非特殊情况,否则不用处理失败逻辑) return data }, async (error: AxiosError) => { const { response } = error tryHideFullScreenLoading() // 请求超时 && 网络错误单独判断,没有 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(url: string, params?: object, _object = {}): Promise> { return this.service.get(url, { params, ..._object }) } post(url: string, params?: object | string, _object = {}): Promise> { return this.service.post(url, params, _object) } put(url: string, params?: object, _object = {}): Promise> { return this.service.put(url, params, _object) } delete(url: string, params?: any, _object = {}): Promise> { return this.service.delete(url, { params, ..._object }) } downloadGet(url: string, params?: object, _object = {}): Promise { return this.service.get(url, { ..._object, responseType: 'blob' }) } downloadPost(url: string, params?: object, _object = {}): Promise { return this.service.post(url, params, { ..._object, responseType: 'blob' }) } } export default new RequestHttp(config)