fix(web): 统一请求失败提示和刷新队列处理

This commit is contained in:
chengqian committed 2026-07-30 14:10:04 +08:00
1 parent 25c9de0465
commit 49eb8d07cc
3 files changed
+113 -46

No files matched your search

+4 -4
View File
@@ -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
+54 -19
View File
@@ -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
}
+55 -23
View File
@@ -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
}