fix(web): suppress duplicate toasts when auth session expires
Desktop Client Build / Resolve Build Metadata (push) Successful in 32s
Frontend CI / Frontend (push) Successful in 4m4s
Desktop Client Build / Publish Client Artifacts to NAS (push) Has been cancelled
Desktop Client Build / Build Desktop Client (push) Has been cancelled

Mark 401/refresh-failure errors as handled so per-view catch blocks fall
through silently while a single global "登录已过期" warning is shown.
Centralize ops-web error rendering via showOpsError.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 00:12:29 +08:00
parent 558646b166
commit ff2bb77cdd
22 changed files with 161 additions and 78 deletions
+16
View File
@@ -0,0 +1,16 @@
import { message } from 'ant-design-vue'
import { isHandledOpsApiError, OpsApiError } from '@/lib/http'
export function showOpsError(error: unknown, fallback = '请求失败'): void {
if (isHandledOpsApiError(error)) {
return
}
if (error instanceof OpsApiError) {
message.error(error.detail || error.message)
return
}
message.error(fallback)
}
+19 -2
View File
@@ -7,6 +7,7 @@ export class OpsApiError extends Error {
code: number
detail?: string
requestId?: string
handled?: boolean
constructor(input: {
message: string
@@ -14,6 +15,7 @@ export class OpsApiError extends Error {
status?: number
detail?: string
requestId?: string
handled?: boolean
}) {
super(input.message)
this.name = 'OpsApiError'
@@ -21,6 +23,7 @@ export class OpsApiError extends Error {
this.status = input.status
this.detail = input.detail
this.requestId = input.requestId
this.handled = input.handled
}
}
@@ -33,11 +36,20 @@ interface ApiEnvelope<T> {
}
let onUnauthorized: (() => void) | null = null
let unauthorizedHandled = false
export function bindUnauthorizedHandler(handler: () => void): void {
onUnauthorized = handler
}
export function resetUnauthorizedHandling(): void {
unauthorizedHandled = false
}
export function isHandledOpsApiError(error: unknown): boolean {
return error instanceof OpsApiError && error.handled === true
}
const client: AxiosInstance = axios.create({
baseURL: '/api/ops',
timeout: 30000,
@@ -63,10 +75,14 @@ client.interceptors.response.use(
(error: AxiosError<ApiEnvelope<unknown>>) => {
const status = error.response?.status
const payload = error.response?.data
const handled = status === 401 && !isLoginRequest(error.config?.url, error.config?.method)
if (status === 401 && !isLoginRequest(error.config?.url, error.config?.method)) {
if (handled) {
clearStoredSession()
onUnauthorized?.()
if (!unauthorizedHandled) {
unauthorizedHandled = true
onUnauthorized?.()
}
}
return Promise.reject(
@@ -76,6 +92,7 @@ client.interceptors.response.use(
status,
detail: typeof payload?.detail === 'string' ? payload.detail : undefined,
requestId: typeof payload?.request_id === 'string' ? payload.request_id : undefined,
handled,
}),
)
},