|
@@ -0,0 +1,210 @@
|
|
|
+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 }
|
|
|
+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)
|
|
|
+ }
|
|
|
+ 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<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 })
|
|
|
+ }
|
|
|
+ downloadGet(url: string, params?: object, _object = {}): Promise<BlobPart> {
|
|
|
+ return this.service.get(url, { ..._object, responseType: 'blob' })
|
|
|
+ }
|
|
|
+ downloadPost(url: string, params?: object, _object = {}): Promise<BlobPart> {
|
|
|
+ return this.service.post(url, params, { ..._object, responseType: 'blob' })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export default new RequestHttp(config)
|