Files
MrleeAdmin/frontend/web/app/utils/request.ts
T

523 lines
21 KiB
TypeScript
Raw Normal View History

2026-01-16 22:04:12 +08:00
import { ElMessage } from 'element-plus'
import qs from 'qs'
2026-04-07 11:01:11 +08:00
import { getToken, getRefreshToken } from './common'
import { useRuntimeConfig, navigateTo, useCookie } from 'nuxt/app'
import { useMemberStore } from '~/stores/member'
2026-08-31 11:45:14 +08:00
import { decryptEncryptedResponse, prepareEncryptedRequest } from '~/utils/security/api-encrypt'
2026-04-07 11:01:11 +08:00
// 响应数据类型定义
interface ResponseData<T = any> {
code: number
msg: string
data: T
type?: string
}
2026-01-16 22:04:12 +08:00
interface ConfigOption {
showErrorMessage?: boolean
showSuccessMessage?: boolean,
2026-08-31 11:45:14 +08:00
headers?: HeadersInit
2026-04-07 11:01:11 +08:00
// 控制是否返回完整响应对象(默认 false 返回 data)
returnFullResponse?: boolean
// 响应类型
responseType?: 'json' | 'blob' | 'text'
2026-01-16 22:04:12 +08:00
}
interface FetchOptions {
baseURL: string
headers: Record<string, any>
onRequest?: (data: any) => void
onResponse?: (data: any) => void
onResponseError?: (data: any) => void
showErrorMessage?: boolean
showSuccessMessage?: boolean
watch: boolean
}
class Http {
private options: FetchOptions = {
baseURL: '',
headers: {},
watch: false
}
2026-04-07 11:01:11 +08:00
private isRefreshing = false
private refreshSubscribers: ((token: string) => void)[] = []
2026-01-16 22:04:12 +08:00
public constructor() {
2026-04-07 11:01:11 +08:00
/**
* 全局请求拦截器
*/
this.options.onRequest = (data) => {
const runtimeConfig = useRuntimeConfig()
2026-01-16 22:04:12 +08:00
2026-04-07 11:01:11 +08:00
this.options.baseURL = (runtimeConfig.public.VITE_APP_BASE_URL as string) || `${location.origin}/api/`
const channelKey = (runtimeConfig.public.VITE_REQUEST_HEADER_CHANNEL_KEY as string) || 'channel'
const tokenKey = (runtimeConfig.public.VITE_REQUEST_HEADER_TOKEN_KEY as string) || 'Authorization'
this.options.headers[channelKey] = 'pc'
const token = getToken()
if (token) {
this.options.headers[tokenKey] = token
} else {
// 清除Authorization头,确保退出登录后不会携带旧token
delete this.options.headers[tokenKey]
2026-01-16 22:04:12 +08:00
}
2026-04-07 11:01:11 +08:00
}
2026-01-16 22:04:12 +08:00
2026-04-07 11:01:11 +08:00
/**
* 全局响应拦截器
*/
this.options.onResponse = ({ response, options }) => {
const { _data: data } = response
this.handleNetworkError(response)
if (data && data.code != undefined) {
if (data.code == 0) {
if (options.showSuccessMessage) ElMessage({ message: data.msg, type: 'success' })
} else {
// 移除这里的错误提示,由 request 方法统一处理,避免重复提示
this.handleAuthError(data.code)
2026-01-16 22:04:12 +08:00
}
}
}
2026-04-07 11:01:11 +08:00
}
2026-01-16 22:04:12 +08:00
2026-04-07 11:01:11 +08:00
public get<T = any>(url: string, query = {}, config: ConfigOption = {}): Promise<T> {
2026-01-16 22:04:12 +08:00
url += '?' + qs.stringify(query)
2026-04-07 11:01:11 +08:00
return this.request<T>(url, 'GET', {}, config)
2026-01-16 22:04:12 +08:00
}
2026-04-07 11:01:11 +08:00
public post<T = any>(url: string, body = {}, config: ConfigOption = {}): Promise<T> {
return this.request<T>(url, 'POST', { body }, config)
2026-01-16 22:04:12 +08:00
}
2026-04-07 11:01:11 +08:00
/**
* 上传文件
*/
public upload<T = any>(url: string, formData: FormData, config: ConfigOption = {}): Promise<T> {
return new Promise<T>((resolve, reject) => {
setTimeout(async () => {
try {
// 处理首次请求baseurl空的问题
const runtimeConfig = useRuntimeConfig()
const baseURL = this.options.baseURL || (runtimeConfig.public.VITE_APP_BASE_URL as string) || `${location.origin}/api/`
// 处理 URL 路径,确保没有多余的斜杠
const normalizedBaseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL
const normalizedUrl = url.startsWith('/') ? url.slice(1) : url
const fullURL = `${normalizedBaseURL}/${normalizedUrl}`
// 执行请求拦截器
if (this.options.onRequest) {
this.options.onRequest({ url: fullURL, method: 'POST' })
}
// 构建请求选项
const requestOptions: RequestInit = {
method: 'POST',
headers: {
...this.options.headers,
...(config.headers || {})
// 不设置 Content-Type,让浏览器自动设置 multipart/form-data
}
}
// FormData 直接作为 body,不进行 JSON.stringify
requestOptions.body = formData
// 添加超时处理
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error('Request timeout'))
}, 60000) // 上传文件超时时间改为60秒
})
const fetchPromise = fetch(fullURL, requestOptions)
const response = await Promise.race([fetchPromise, timeoutPromise])
const data = await response.json()
// 执行响应拦截器
if (this.options.onResponse) {
this.options.onResponse({ response: { _data: data }, options: config })
}
// 处理响应数据
if (data && data.code !== undefined) {
// 特殊处理 401 和 403
if (data.code == 401 || data.code == 403) {
this.handleAuthError(data.code)
}
// code != 0 时的处理
if (data.code != 0) {
if (config.showErrorMessage === false) {
if (config.returnFullResponse) {
resolve(data as T)
} else {
resolve(data.data as T)
}
return
}
ElMessage({ message: data.msg || '请求失败', type: 'error' })
reject(new Error(data.msg || '请求失败'))
return
}
// code == 0 时根据配置决定返回什么
if (config.returnFullResponse) {
resolve(data as T)
} else {
resolve(data.data as T)
}
} else {
resolve(data)
}
} catch (error) {
console.error('[upload] Error:', error)
ElMessage({ message: '网络请求失败', type: 'error' })
reject(error)
}
}, this.options.baseURL ? 0 : 500)
})
2026-01-16 22:04:12 +08:00
}
2026-04-07 11:01:11 +08:00
public put<T = any>(url: string, body = {}, config: ConfigOption = {}): Promise<T> {
return this.request<T>(url, 'PUT', { body }, config)
}
public delete<T = any>(url: string, body = {}, config: ConfigOption = {}): Promise<T> {
return this.request<T>(url, 'DELETE', { body }, config)
2026-01-16 22:04:12 +08:00
}
/**
* 发送请求
* @param url
* @param method
* @param param
* @param config
*/
2026-04-07 11:01:11 +08:00
private request<T = any>(url: string, method: string, param: AnyObject = {}, config: ConfigOption = {}): Promise<T> {
return new Promise<T>((resolve, reject) => {
setTimeout(async () => {
try {
// 处理首次请求baseurl空的问题
const runtimeConfig = useRuntimeConfig()
const baseURL = this.options.baseURL || (runtimeConfig.public.VITE_APP_BASE_URL as string) || `${location.origin}/api/`
// 处理 URL 路径,确保没有多余的斜杠
const normalizedBaseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL
const normalizedUrl = url.startsWith('/') ? url.slice(1) : url
const fullURL = `${normalizedBaseURL}/${normalizedUrl}`
// 执行请求拦截器
if (this.options.onRequest) {
this.options.onRequest({ url: fullURL, method })
2026-01-16 22:04:12 +08:00
}
2026-04-07 11:01:11 +08:00
// 构建请求选项
2026-08-31 11:45:14 +08:00
const requestHeaders: Record<string, string> = {
'Content-Type': 'application/json',
...(this.options.headers as Record<string, string>),
...((config.headers || {}) as Record<string, string>),
...((param.headers || {}) as Record<string, string>)
}
2026-04-07 11:01:11 +08:00
const requestOptions: RequestInit = {
method,
2026-08-31 11:45:14 +08:00
headers: requestHeaders
2026-04-07 11:01:11 +08:00
}
2026-08-31 11:45:14 +08:00
let encryptSession: any = null
2026-04-07 11:01:11 +08:00
// 添加请求体
2026-08-31 11:45:14 +08:00
const encryptedRequest = await prepareEncryptedRequest(baseURL, `/${normalizedUrl}`, method, param.body)
encryptSession = encryptedRequest.session
Object.assign(requestHeaders, encryptedRequest.headers)
if (encryptedRequest.body) {
requestOptions.body = encryptedRequest.body
2026-04-07 11:01:11 +08:00
}
// 发送请求
// 添加超时处理
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error('Request timeout'))
}, 30000) // 30秒超时
})
const fetchPromise = fetch(fullURL, requestOptions)
const response = await Promise.race([fetchPromise, timeoutPromise])
// 根据 responseType 处理响应
let data
const responseType = config.responseType || 'json'
if (responseType === 'json') {
data = await response.json()
2026-08-31 11:45:14 +08:00
data = await decryptEncryptedResponse(data, encryptSession)
2026-04-07 11:01:11 +08:00
} else if (responseType === 'blob') {
// 对于blob类型,直接读取blob并返回
try {
const blob = await response.blob()
2026-08-31 11:45:14 +08:00
resolve(blob as T)
2026-04-07 11:01:11 +08:00
return
} catch (error) {
console.error('[request] Error reading blob:', error)
reject(error)
return
}
} else if (responseType === 'text') {
data = await response.text()
} else {
data = await response.json()
}
// 执行响应拦截器(仅对 JSON 响应执行)
if (responseType === 'json' && this.options.onResponse) {
this.options.onResponse({ response: { _data: data }, options: config })
}
// 处理响应数据
if (responseType === 'json' && data && data.code !== undefined) {
// 特殊处理 401 和 403
if (data.code == 401) {
console.log('[request] Got 401, checking refreshToken...')
// 检查是否有 refreshToken,如果没有则直接跳转到登录
const memberStore = useMemberStore()
// 直接从 localStorage 读取验证
const stored = localStorage.getItem('member-store')
console.log('[request] localStorage:', stored)
console.log('[request] memberStore.refreshToken:', !!memberStore.refreshToken)
let hasRefreshToken = !!memberStore.refreshToken
if (!hasRefreshToken) {
console.log('[request] No refreshToken in store')
memberStore.logout()
reject(new Error('登录已过期,请重新登录'))
return
2026-01-16 22:04:12 +08:00
}
2026-04-07 11:01:11 +08:00
console.log('[request] Has refreshToken, attempting refresh...')
// 尝试刷新 token
const originalRequest = {
url: fullURL,
method,
options: requestOptions,
config
}
this.handle401Error(originalRequest).then((newToken) => {
console.log('[request] New token:', newToken?.substring(0, 20) + '...')
// 重新发送请求
this.options.headers['Authorization'] = `Bearer ${newToken}`
2026-08-31 11:45:14 +08:00
requestHeaders['Authorization'] = `Bearer ${newToken}`
2026-04-07 11:01:11 +08:00
return fetch(fullURL, requestOptions)
}).then(async (newResponse) => {
2026-08-31 11:45:14 +08:00
let newData = await newResponse.json()
newData = await decryptEncryptedResponse(newData, encryptSession)
2026-04-07 11:01:11 +08:00
if (newData.code == 0) {
if (config.returnFullResponse) {
resolve(newData as T)
} else {
resolve(newData.data as T)
}
} else {
ElMessage({ message: newData.msg || '请求失败', type: 'error' })
reject(new Error(newData.msg || '请求失败'))
}
}).catch((error) => {
console.error('[request] Error refreshing token:', error)
reject(error)
})
return
} else if (data.code == 403) {
this.handleAuthError(data.code)
}
// code != 0 时的处理
if (data.code != 0) {
// 如果配置了 showErrorMessage: false,则不中断,返回 data
if (config.showErrorMessage === false) {
if (config.returnFullResponse) {
resolve(data as T)
} else {
resolve(data.data as T)
}
return
}
// 否则显示错误提示并中断
ElMessage({ message: data.msg || '请求失败', type: 'error' })
reject(new Error(data.msg || '请求失败'))
return
}
// code == 0 时根据配置决定返回什么
if (config.returnFullResponse) {
// 返回完整对象
resolve(data as T)
} else {
// 默认只返回 data
resolve(data.data as T)
2026-01-16 22:04:12 +08:00
}
} else {
2026-04-07 11:01:11 +08:00
// 非 JSON 响应直接返回
resolve(data as T)
2026-01-16 22:04:12 +08:00
}
2026-04-07 11:01:11 +08:00
} catch (error) {
console.error('[request] Error:', error)
ElMessage({ message: '网络请求失败', type: 'error' })
reject(error)
}
2026-01-16 22:04:12 +08:00
}, this.options.baseURL ? 0 : 500);
})
}
2026-04-07 11:01:11 +08:00
private async handle401Error(originalRequest: any): Promise<string> {
if (this.isRefreshing) {
// 如果正在刷新 token,将请求加入队列
return new Promise((resolve) => {
this.refreshSubscribers.push((token) => {
originalRequest.options.headers['Authorization'] = `Bearer ${token}`
resolve(token)
})
})
}
this.isRefreshing = true
try {
// 刷新 token
const newToken = await this.refreshToken()
// 通知所有队列中的请求
this.refreshSubscribers.forEach((callback) => callback(newToken))
this.refreshSubscribers = []
return newToken
} catch (error) {
// 刷新失败,跳转到登录页
useMemberStore().logout()
throw error
} finally {
this.isRefreshing = false
}
}
private async refreshToken(): Promise<string> {
const runtimeConfig = useRuntimeConfig()
let baseURL = this.options.baseURL || (runtimeConfig.public.VITE_APP_BASE_URL as string) || `${location.origin}/api`
// 移除末尾斜杠,统一 URL 拼接
baseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL
// 从 store 中获取 refresh token
const memberStore = useMemberStore()
let refreshToken = memberStore.refreshToken
// 如果 store 中没有,尝试从 cookie 获取
if (!refreshToken) {
const cookieRefreshToken = useCookie('refreshToken').value
if (cookieRefreshToken) {
refreshToken = cookieRefreshToken
}
}
if (!refreshToken) {
throw new Error('No refresh token available')
}
const response = await fetch(`${baseURL}/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refresh_token: refreshToken })
})
const data = await response.json()
if (data.code == 0 && data.data) {
const { access_token, refresh_token } = data.data
// 更新 store 中的 token
await memberStore.setToken(access_token, refresh_token)
return access_token
} else {
throw new Error(data.msg || 'Failed to refresh token')
}
}
2026-01-16 22:04:12 +08:00
private handleAuthError(code: number) {
switch (code) {
case 401:
2026-04-07 11:01:11 +08:00
// 401 错误由 handle401Error 处理
2026-01-16 22:04:12 +08:00
break;
case 402:
navigateTo('/site/close', { replace: true })
break;
}
}
private handleNetworkError(err: any) {
if (err.status && err.status != 200) {
let errMessage = ''
switch (err.status) {
case 400:
2026-04-07 11:01:11 +08:00
errMessage = t('common.request.400')
2026-01-16 22:04:12 +08:00
break
case 401:
2026-04-07 11:01:11 +08:00
errMessage = t('common.request.401')
// 401 错误由 handle401Error 处理
2026-01-16 22:04:12 +08:00
break
case 403:
2026-04-07 11:01:11 +08:00
errMessage = t('common.request.403')
2026-01-16 22:04:12 +08:00
break
case 404:
2026-04-07 11:01:11 +08:00
errMessage = err.url + t('common.request.404')
2026-01-16 22:04:12 +08:00
break
case 405:
2026-04-07 11:01:11 +08:00
errMessage = t('common.request.405')
2026-01-16 22:04:12 +08:00
break
case 408:
2026-04-07 11:01:11 +08:00
errMessage = t('common.request.408')
2026-01-16 22:04:12 +08:00
break
case 409:
2026-04-07 11:01:11 +08:00
errMessage = t('common.request.409')
2026-01-16 22:04:12 +08:00
break
case 500:
2026-04-07 11:01:11 +08:00
errMessage = t('common.request.500')
2026-01-16 22:04:12 +08:00
break
case 501:
2026-04-07 11:01:11 +08:00
errMessage = t('common.request.501')
2026-01-16 22:04:12 +08:00
break
case 502:
2026-04-07 11:01:11 +08:00
errMessage = t('common.request.502')
2026-01-16 22:04:12 +08:00
break
case 503:
2026-04-07 11:01:11 +08:00
errMessage = t('common.request.503')
2026-01-16 22:04:12 +08:00
break
case 504:
2026-04-07 11:01:11 +08:00
errMessage = t('common.request.504')
2026-01-16 22:04:12 +08:00
break
case 505:
2026-04-07 11:01:11 +08:00
errMessage = t('common.request.505')
2026-01-16 22:04:12 +08:00
break
}
2026-04-07 11:01:11 +08:00
if (errMessage) {
ElMessage({ message: errMessage, type: 'error' })
}
2026-01-16 22:04:12 +08:00
}
}
}
const request = new Http()
export default request
2026-04-07 11:01:11 +08:00