fix(web): 统一请求失败提示和刷新队列处理
This commit is contained in:
1 parent
25c9de0465
commit
49eb8d07cc
3 files changed
+113
-46
No files matched your search
@@ -2,10 +2,9 @@ import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import adminRoutes from './admin'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { shouldRefresh, getRefreshToken } from '@/utils/request'
|
||||
import request, { shouldRefresh, getRefreshToken } from '@/utils/request'
|
||||
import { useSiteName } from '@/composables/useSiteName'
|
||||
import { useTopProgress } from '@/composables/useTopProgress'
|
||||
import axios from 'axios'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
@@ -35,10 +34,11 @@ export function markSystemInitialized(): void {
|
||||
async function checkSetupStatus(): Promise<boolean> {
|
||||
if (setupChecked) return systemInitialized === true
|
||||
try {
|
||||
const res = await axios.get('/api/setup/status', { timeout: 5000 })
|
||||
const res = await request.get('/setup/status', { timeout: 5000 })
|
||||
systemInitialized = res.data?.data?.initialized === true
|
||||
} catch {
|
||||
systemInitialized = true
|
||||
// Allow the current navigation, but retry the setup check next time.
|
||||
return true
|
||||
}
|
||||
setupChecked = true
|
||||
return systemInitialized === true
|
||||
|
||||
@@ -82,7 +82,12 @@ export function shouldRefresh(): boolean {
|
||||
}
|
||||
|
||||
let isRefreshing = false
|
||||
let pendingRequests: Array<(token: string) => void> = []
|
||||
interface PendingRequest {
|
||||
resolve: (token: string) => void
|
||||
reject: (error: unknown) => void
|
||||
}
|
||||
|
||||
let pendingRequests: PendingRequest[] = []
|
||||
let tokenRefreshedCallback: ((tokens: TokenPair) => void) | null = null
|
||||
|
||||
export function onTokenRefreshed(cb: (tokens: TokenPair) => void): void {
|
||||
@@ -90,14 +95,21 @@ export function onTokenRefreshed(cb: (tokens: TokenPair) => void): void {
|
||||
}
|
||||
|
||||
function replayPending(token: string): void {
|
||||
pendingRequests.forEach((cb) => cb(token))
|
||||
pendingRequests.forEach(({ resolve }) => resolve(token))
|
||||
pendingRequests = []
|
||||
}
|
||||
|
||||
function clearPending(): void {
|
||||
function rejectPending(error: unknown): void {
|
||||
pendingRequests.forEach(({ reject }) => reject(error))
|
||||
pendingRequests = []
|
||||
}
|
||||
|
||||
function waitForRefresh(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingRequests.push({ resolve, reject })
|
||||
})
|
||||
}
|
||||
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 30_000,
|
||||
@@ -132,18 +144,18 @@ request.interceptors.request.use(
|
||||
async (config: InternalAxiosRequestConfig) => {
|
||||
if (shouldRefresh() && !config.url?.includes('/auth/refresh')) {
|
||||
if (isRefreshing) {
|
||||
await new Promise<void>((resolve) => {
|
||||
pendingRequests.push(() => resolve())
|
||||
})
|
||||
await waitForRefresh()
|
||||
} else {
|
||||
isRefreshing = true
|
||||
try {
|
||||
const newTokens = await doRefresh()
|
||||
replayPending(newTokens.accessToken)
|
||||
} catch {
|
||||
clearPending()
|
||||
} catch (error) {
|
||||
rejectPending(error)
|
||||
clearTokens()
|
||||
showRequestError(error, config)
|
||||
window.location.hash = '#/admin/login'
|
||||
return Promise.reject(error)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
@@ -168,6 +180,26 @@ function showErrorToast(message: string, config?: InternalAxiosRequestConfig) {
|
||||
Message.error(message)
|
||||
}
|
||||
|
||||
function showRequestError(error: unknown, config?: InternalAxiosRequestConfig) {
|
||||
if (config?._suppressErrorMsg || !axios.isAxiosError(error)) return
|
||||
if ((error as any)._errorToastShown) return
|
||||
;(error as any)._errorToastShown = true
|
||||
const responseData = error.response?.data
|
||||
if (responseData && typeof responseData === 'object' && 'message' in responseData) {
|
||||
Message.error(String(responseData.message || '请求失败'))
|
||||
return
|
||||
}
|
||||
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
||||
Message.error('请求超时,请稍后重试')
|
||||
return
|
||||
}
|
||||
if (!error.response) {
|
||||
Message.error('网络连接异常,请检查后端服务是否正常')
|
||||
return
|
||||
}
|
||||
Message.error(`服务器请求失败(${error.response.status})`)
|
||||
}
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const data = response.data
|
||||
@@ -204,18 +236,20 @@ request.interceptors.response.use(
|
||||
;(err as any).isDemoModeError = error.response.data.code === DEMO_MODE_CODE
|
||||
return Promise.reject(err)
|
||||
}
|
||||
showRequestError(error, originalRequest)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve) => {
|
||||
pendingRequests.push((newToken: string) => {
|
||||
originalRequest.headers = originalRequest.headers ?? {}
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
originalRequest._retry = true
|
||||
resolve(request(originalRequest))
|
||||
})
|
||||
})
|
||||
try {
|
||||
const newToken = await waitForRefresh()
|
||||
originalRequest.headers = originalRequest.headers ?? {}
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
originalRequest._retry = true
|
||||
return request(originalRequest)
|
||||
} catch (refreshError) {
|
||||
return Promise.reject(refreshError)
|
||||
}
|
||||
}
|
||||
|
||||
originalRequest._retry = true
|
||||
@@ -228,11 +262,12 @@ request.interceptors.response.use(
|
||||
originalRequest.headers = originalRequest.headers ?? {}
|
||||
originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`
|
||||
return request(originalRequest)
|
||||
} catch {
|
||||
clearPending()
|
||||
} catch (refreshError) {
|
||||
rejectPending(refreshError)
|
||||
clearTokens()
|
||||
showRequestError(refreshError, originalRequest)
|
||||
window.location.hash = '#/admin/login'
|
||||
return Promise.reject(error)
|
||||
return Promise.reject(refreshError)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
|
||||
@@ -82,7 +82,12 @@ export function shouldRefresh(): boolean {
|
||||
}
|
||||
|
||||
let isRefreshing = false
|
||||
let pendingRequests: Array<(token: string) => void> = []
|
||||
interface PendingRequest {
|
||||
resolve: (token: string) => void
|
||||
reject: (error: unknown) => void
|
||||
}
|
||||
|
||||
let pendingRequests: PendingRequest[] = []
|
||||
let tokenRefreshedCallback: ((tokens: TokenPair) => void) | null = null
|
||||
|
||||
export function onTokenRefreshed(cb: (tokens: TokenPair) => void): void {
|
||||
@@ -90,14 +95,21 @@ export function onTokenRefreshed(cb: (tokens: TokenPair) => void): void {
|
||||
}
|
||||
|
||||
function replayPending(token: string): void {
|
||||
pendingRequests.forEach((cb) => cb(token))
|
||||
pendingRequests.forEach(({ resolve }) => resolve(token))
|
||||
pendingRequests = []
|
||||
}
|
||||
|
||||
function clearPending(): void {
|
||||
function rejectPending(error: unknown): void {
|
||||
pendingRequests.forEach(({ reject }) => reject(error))
|
||||
pendingRequests = []
|
||||
}
|
||||
|
||||
function waitForRefresh(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingRequests.push({ resolve, reject })
|
||||
})
|
||||
}
|
||||
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 30_000,
|
||||
@@ -149,18 +161,18 @@ request.interceptors.request.use(
|
||||
async (config: InternalAxiosRequestConfig) => {
|
||||
if (!isPublicPath(config.url) && shouldRefresh() && !config.url?.includes('/auth/refresh')) {
|
||||
if (isRefreshing) {
|
||||
await new Promise<void>((resolve) => {
|
||||
pendingRequests.push(() => resolve())
|
||||
})
|
||||
await waitForRefresh()
|
||||
} else {
|
||||
isRefreshing = true
|
||||
try {
|
||||
const newTokens = await doRefresh()
|
||||
replayPending(newTokens.accessToken)
|
||||
} catch {
|
||||
clearPending()
|
||||
} catch (error) {
|
||||
rejectPending(error)
|
||||
clearTokens()
|
||||
showRequestError(error, config)
|
||||
window.location.hash = '#/tenant/login'
|
||||
return Promise.reject(error)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
@@ -185,6 +197,27 @@ function showErrorToast(message: string, config?: InternalAxiosRequestConfig | A
|
||||
toast.error(message)
|
||||
}
|
||||
|
||||
function showRequestError(error: unknown, config?: InternalAxiosRequestConfig | AxiosRequestConfig) {
|
||||
if (config && '_suppressErrorMsg' in config && (config as any)._suppressErrorMsg) return
|
||||
if (!axios.isAxiosError(error)) return
|
||||
if ((error as any)._errorToastShown) return
|
||||
;(error as any)._errorToastShown = true
|
||||
const responseData = error.response?.data
|
||||
if (responseData && typeof responseData === 'object' && 'message' in responseData) {
|
||||
toast.error(String(responseData.message || '请求失败'))
|
||||
return
|
||||
}
|
||||
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
||||
toast.error('请求超时,请稍后重试')
|
||||
return
|
||||
}
|
||||
if (!error.response) {
|
||||
toast.error('网络连接异常,请检查后端服务是否正常')
|
||||
return
|
||||
}
|
||||
toast.error(`服务器请求失败(${error.response.status})`)
|
||||
}
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const data = response.data
|
||||
@@ -222,22 +255,20 @@ request.interceptors.response.use(
|
||||
;(err as any).isDemoModeError = error.response.data.code === DEMO_MODE_CODE
|
||||
return Promise.reject(err)
|
||||
}
|
||||
// Network or other unexpected errors
|
||||
if (!error.response) {
|
||||
showErrorToast('网络连接异常,请稍后重试', originalRequest)
|
||||
}
|
||||
showRequestError(error, originalRequest)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve) => {
|
||||
pendingRequests.push((newToken: string) => {
|
||||
originalRequest.headers = originalRequest.headers ?? {}
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
originalRequest._retry = true
|
||||
resolve(request(originalRequest))
|
||||
})
|
||||
})
|
||||
try {
|
||||
const newToken = await waitForRefresh()
|
||||
originalRequest.headers = originalRequest.headers ?? {}
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
originalRequest._retry = true
|
||||
return request(originalRequest)
|
||||
} catch (refreshError) {
|
||||
return Promise.reject(refreshError)
|
||||
}
|
||||
}
|
||||
|
||||
originalRequest._retry = true
|
||||
@@ -250,11 +281,12 @@ request.interceptors.response.use(
|
||||
originalRequest.headers = originalRequest.headers ?? {}
|
||||
originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`
|
||||
return request(originalRequest)
|
||||
} catch {
|
||||
clearPending()
|
||||
} catch (refreshError) {
|
||||
rejectPending(refreshError)
|
||||
clearTokens()
|
||||
showRequestError(refreshError, originalRequest)
|
||||
window.location.hash = '#/tenant/login'
|
||||
return Promise.reject(error)
|
||||
return Promise.reject(refreshError)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user