Files
aiot-uniapp/src/http/http.ts

224 lines
7.4 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 type { IDoubleTokenRes } from '@/api/types/login'
import type { CustomRequestOptions, IResponse } from '@/http/types'
import { nextTick } from 'vue'
import { useTokenStore } from '@/store/token'
import { getLastPage, isDoubleTokenMode } from '@/utils'
import { ApiEncrypt } from '@/utils/encrypt'
import { toLoginPage } from '@/utils/toLoginPage'
import { ResultEnum } from './tools/enum'
// 刷新 token 状态管理
let refreshing = false // 防止重复刷新 token 标识
let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
export function http<T>(options: CustomRequestOptions) {
// 1. 返回 Promise 对象
return new Promise<T>((resolve, reject) => {
uni.request({
...options,
dataType: 'json',
// #ifndef MP-WEIXIN
responseType: 'json',
// #endif
// 响应成功
success: async (res) => {
let responseData = res.data as IResponse<T>
// 检查是否需要解密响应数据
const encryptHeader = ApiEncrypt.getEncryptHeader()
const isEncryptResponse = res.header[encryptHeader] === 'true' || res.header[encryptHeader.toLowerCase()] === 'true'
if (isEncryptResponse && typeof responseData === 'string') {
try {
// 解密响应数据
responseData = ApiEncrypt.decryptResponse(responseData)
} catch (error) {
console.error('响应数据解密失败:', error)
throw new Error(`响应数据解密失败: ${(error as Error).message}`)
}
}
const { code } = responseData
// 检查是否是401错误包括HTTP状态码401或业务码401
const isTokenExpired = res.statusCode === 401 || code === 401
if (isTokenExpired) {
const tokenStore = useTokenStore()
if (!isDoubleTokenMode) {
// 未启用双token策略清理用户信息跳转到登录页
tokenStore.logout()
toLoginPage()
return reject(res)
}
/* -------- 无感刷新 token ----------- */
const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
// token 失效的,且有刷新 token 的,才放到请求队列里
if (refreshToken) {
taskQueue.push(() => {
resolve(http<T>(options))
})
}
// 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
if (refreshToken && !refreshing) {
refreshing = true
try {
// 发起刷新 token 请求(使用 store 的 refreshToken 方法)
await tokenStore.refreshToken()
// 刷新 token 成功
refreshing = false
nextTick(() => {
// 关闭其他弹窗
// 注释 by 芋艿:刷新 token 成功,是后台静默操作,没必要提示用户
// uni.hideToast()
// uni.showToast({
// title: 'token 刷新成功',
// icon: 'none',
// })
})
// 将任务队列的所有任务重新请求
taskQueue.forEach(task => task())
} catch (refreshErr) {
console.error('刷新 token 失败:', refreshErr)
refreshing = false
// 刷新 token 失败,跳转到登录页
nextTick(() => {
// 关闭其他弹窗
uni.hideToast()
uni.showToast({
title: '登录已过期,请重新登录',
icon: 'none',
})
})
// 清除用户信息
await tokenStore.logout()
// 跳转到登录页
setTimeout(() => {
// 优化 by 芋艿:跳转登录页时,携带上次浏览的页面地址,登录成功后可以跳回去
const lastPage = getLastPage()
let queryString = ''
if (lastPage) {
const fullPath = lastPage.$page?.fullPath || `/${lastPage.route}`
queryString = `?redirect=${encodeURIComponent(fullPath)}`
}
toLoginPage({ queryString })
}, 2000)
} finally {
// 不管刷新 token 成功与否,都清空任务队列
taskQueue = []
}
}
return reject(res)
}
// 处理其他成功状态HTTP状态码200-299
if (res.statusCode >= 200 && res.statusCode < 300) {
// add by panda 25.12.10:如果设置了 original 为 true则返回原始数据。例如说滑块验证码有自己的返回格式
if (options.original) {
return resolve(responseData as unknown as T)
}
// 处理业务逻辑错误
if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
// add by 芋艿:后端返回的 msg 提示
!options.hideErrorToast
&& uni.showToast({
icon: 'none',
title: responseData.msg || responseData.message || '请求错误',
})
// add by 芋艿reject 替代原本的 resolve避免调用的地方以为请求成功
return reject(responseData)
}
return resolve(responseData.data)
}
// 处理其他错误
!options.hideErrorToast
&& uni.showToast({
icon: 'none',
title: (res.data as any).msg || '请求错误',
})
reject(res)
},
// 响应失败
fail(err) {
uni.showToast({
icon: 'none',
title: '网络错误,换个网络试试',
})
reject(err)
},
})
})
}
/**
* GET 请求
* @param url 后台地址
* @param query 请求query参数
* @param header 请求头默认为json格式
* @param options 其他配置项
* @returns
*/
export function httpGet<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
return http<T>({
url,
query,
method: 'GET',
header,
...options,
})
}
/**
* POST 请求
* @param url 后台地址
* @param data 请求body参数
* @param query 请求query参数post请求也支持query很多微信接口都需要
* @param header 请求头默认为json格式
* @param options 其他配置项
* @returns
*/
export function httpPost<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
return http<T>({
url,
query,
data,
method: 'POST',
header,
...options,
})
}
/**
* PUT 请求
*/
export function httpPut<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
return http<T>({
url,
data,
query,
method: 'PUT',
header,
...options,
})
}
/**
* DELETE 请求(无请求体,仅 query
*/
export function httpDelete<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
return http<T>({
url,
data,
query,
method: 'DELETE',
header,
...options,
})
}
// 支持与 axios 类似的API调用
http.get = httpGet
http.post = httpPost
http.put = httpPut
http.delete = httpDelete