diff --git a/web/admin/src/router/index.ts b/web/admin/src/router/index.ts index 08a104f..e0f3b09 100644 --- a/web/admin/src/router/index.ts +++ b/web/admin/src/router/index.ts @@ -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 { 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 diff --git a/web/admin/src/utils/request.ts b/web/admin/src/utils/request.ts index 52d6919..112a7ba 100644 --- a/web/admin/src/utils/request.ts +++ b/web/admin/src/utils/request.ts @@ -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 { + 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((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 } diff --git a/web/tenant/src/utils/request.ts b/web/tenant/src/utils/request.ts index d56b413..1d65c96 100644 --- a/web/tenant/src/utils/request.ts +++ b/web/tenant/src/utils/request.ts @@ -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 { + 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((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 }