index.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from 'axios'
  2. import { showFullScreenLoading, tryHideFullScreenLoading } from '@/components/Loading/fullScreen'
  3. import { LOGIN_URL } from '@/config'
  4. import { ElMessage, ElMessageBox } from 'element-plus'
  5. import { ResultData } from '@/api/interface'
  6. import { ResultEnum } from '@/enums/httpEnum'
  7. import { checkStatus } from './helper/checkStatus'
  8. import { AxiosCanceler } from './helper/axiosCancel'
  9. import { useUserStore } from '@/stores/modules/user'
  10. import router from '@/routers'
  11. import { getToken } from '@/utils/token'
  12. import { tansParams } from '@/utils/common'
  13. import { nanoid } from 'nanoid'
  14. import cache from '@/plugins/cache'
  15. import { encryptBase64, encryptWithAes, generateAesKey, decryptWithAes, decryptBase64 } from '@/utils/crypto'
  16. import { encrypt, decrypt } from '@/utils/jsEncrypt'
  17. export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
  18. loading?: boolean
  19. cancel?: boolean
  20. isEncrypt?: boolean
  21. }
  22. const config = {
  23. // 默认地址请求地址,可在 .env.** 文件中修改
  24. baseURL: import.meta.env.VITE_API_URL as string,
  25. // 设置超时时间
  26. timeout: ResultEnum.TIMEOUT as number,
  27. // 跨域时候允许携带凭证
  28. withCredentials: true
  29. }
  30. const encryptHeader = 'encrypt-key'
  31. // 是否显示重新登录
  32. export let isReLogin = { show: false }
  33. axios.defaults.headers['clientid'] = import.meta.env.VITE_APP_CLIENT_ID
  34. const axiosCanceler = new AxiosCanceler()
  35. class RequestHttp {
  36. service: AxiosInstance
  37. public constructor(config: AxiosRequestConfig) {
  38. // instantiation
  39. this.service = axios.create(config)
  40. /**
  41. * @description 请求拦截器
  42. * 客户端发送请求 -> [请求拦截器] -> 服务器
  43. * token校验(JWT) : 接受服务器返回的 token,存储到 vuex/pinia/本地储存当中
  44. */
  45. this.service.interceptors.request.use(
  46. (config: CustomAxiosRequestConfig) => {
  47. // 是否需要加密
  48. const isEncrypt = config.isEncrypt ?? (config.isEncrypt = false)
  49. // 重复请求不需要取消,在 api 服务中通过指定的第三个参数: { cancel: false } 来控制
  50. config.cancel ?? (config.cancel = true)
  51. config.cancel && axiosCanceler.addPending(config)
  52. // 当前请求不需要显示 loading,在 api 服务中通过指定的第三个参数: { loading: false } 来控制
  53. config.loading ?? (config.loading = true)
  54. config.loading && showFullScreenLoading()
  55. // 是否需要设置 token
  56. const isToken = (config.headers || {}).isToken === false
  57. // 是否需要防止数据重复提交
  58. const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
  59. const requestId = nanoid()
  60. const userStore = useUserStore()
  61. if (getToken() && !isToken) {
  62. // 请求加上随机ID
  63. config.headers.set('RequestId', requestId)
  64. if (config.headers && typeof config.headers.set === 'function') {
  65. config.headers.set('Authorization', 'Bearer ' + userStore.token)
  66. }
  67. }
  68. // get请求映射params参数
  69. if (config.method === 'get' && config.params) {
  70. let url = config.url + '?' + tansParams(config.params)
  71. url = url.slice(0, -1)
  72. config.params = {}
  73. config.url = url
  74. }
  75. if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
  76. const requestObj = {
  77. url: config.url,
  78. data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
  79. time: new Date().getTime()
  80. }
  81. const sessionObj = cache.session.getJSON('sessionObj')
  82. if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
  83. cache.session.setJSON('sessionObj', requestObj)
  84. } else {
  85. const s_url = sessionObj.url // 请求地址
  86. const s_data = sessionObj.data // 请求数据
  87. const s_time = sessionObj.time // 请求时间
  88. const interval = 1000 // 间隔时间(ms),小于此时间视为重复提交
  89. if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
  90. const message = '数据正在处理,请勿重复提交'
  91. console.warn(`[${s_url}]: ` + message)
  92. return Promise.reject(new Error(message))
  93. } else {
  94. cache.session.setJSON('sessionObj', requestObj)
  95. }
  96. }
  97. }
  98. // 当开启参数加密
  99. if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
  100. // 生成一个 AES 密钥
  101. const aesKey = generateAesKey()
  102. config.headers[encryptHeader] = encrypt(encryptBase64(aesKey))
  103. config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey)
  104. }
  105. return config
  106. },
  107. (error: AxiosError) => {
  108. console.log(error)
  109. return Promise.reject(error)
  110. }
  111. )
  112. /**
  113. * @description 响应拦截器
  114. * 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
  115. */
  116. this.service.interceptors.response.use(
  117. (response: AxiosResponse) => {
  118. // 加密后的 AES 秘钥
  119. const keyStr = response.headers[encryptHeader]
  120. // 加密
  121. if (keyStr != null && keyStr != '') {
  122. const data = response.data
  123. // 请求体 AES 解密
  124. const base64Str = decrypt(keyStr)
  125. // base64 解码 得到请求头的 AES 秘钥
  126. const aesKey = decryptBase64(base64Str.toString())
  127. // aesKey 解码 data
  128. const decryptData = decryptWithAes(data, aesKey)
  129. // 将结果 (得到的是 JSON 字符串) 转为 JSON
  130. response.data = JSON.parse(decryptData)
  131. }
  132. const { data, config, request } = response
  133. axiosCanceler.removePending(config)
  134. tryHideFullScreenLoading()
  135. if (request.responseType === 'blob' || request.responseType === 'arraybuffer') {
  136. return data
  137. }
  138. // 登录失效
  139. if (data.code == ResultEnum.OVERDUE) {
  140. const userStore = useUserStore()
  141. if (!isReLogin.show) {
  142. isReLogin.show = true
  143. ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
  144. confirmButtonText: '重新登录',
  145. cancelButtonText: '取消',
  146. type: 'warning'
  147. })
  148. .then(() => {
  149. isReLogin.show = false
  150. userStore.logOut().then(() => {
  151. router.replace(LOGIN_URL)
  152. })
  153. })
  154. .catch(() => {
  155. isReLogin.show = false
  156. })
  157. }
  158. return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
  159. }
  160. // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错)
  161. if (data.code && data.code == ResultEnum.ERROR) {
  162. ElMessage.error(data.msg)
  163. return Promise.reject(data)
  164. }
  165. // 成功请求(在页面上除非特殊情况,否则不用处理失败逻辑)
  166. return data
  167. },
  168. async (error: AxiosError) => {
  169. const { response } = error
  170. tryHideFullScreenLoading()
  171. // 请求超时 && 网络错误单独判断,没有 response
  172. if (error.message.indexOf('timeout') !== -1) ElMessage.error('请求超时!请您稍后重试')
  173. if (error.message.indexOf('Network Error') !== -1) ElMessage.error('网络错误!请您稍后重试')
  174. // 根据服务器响应的错误状态码,做不同的处理
  175. if (response) checkStatus(response.status)
  176. // 服务器结果都没有返回(可能服务器错误可能客户端断网),断网处理:可以跳转到断网页面
  177. if (!window.navigator.onLine) router.replace('/500')
  178. return Promise.reject(error)
  179. }
  180. )
  181. }
  182. /**
  183. * @description 常用请求方法封装
  184. */
  185. get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
  186. return this.service.get(url, { params, ..._object })
  187. }
  188. post<T>(url: string, params?: object | string, _object = {}): Promise<ResultData<T>> {
  189. return this.service.post(url, params, _object)
  190. }
  191. put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
  192. return this.service.put(url, params, _object)
  193. }
  194. delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {
  195. return this.service.delete(url, { params, ..._object })
  196. }
  197. downloadGet(url: string, params?: object, _object = {}): Promise<BlobPart> {
  198. return this.service.get(url, { ..._object, responseType: 'blob' })
  199. }
  200. downloadPost(url: string, params?: object, _object = {}): Promise<BlobPart> {
  201. return this.service.post(url, params, { ..._object, responseType: 'blob' })
  202. }
  203. }
  204. export default new RequestHttp(config)