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
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:
@@ -193,6 +193,7 @@ function authRequiredError(): ApiClientError {
|
|||||||
message: 'not_authenticated',
|
message: 'not_authenticated',
|
||||||
code: 40100,
|
code: 40100,
|
||||||
status: 401,
|
status: 401,
|
||||||
|
handled: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +201,13 @@ function expireStoredSession(): void {
|
|||||||
clearStoredSession({ notifyAuthExpired: true })
|
clearStoredSession({ notifyAuthExpired: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markAuthErrorHandled(error: unknown): unknown {
|
||||||
|
if (error instanceof ApiClientError && error.status === 401) {
|
||||||
|
error.handled = true
|
||||||
|
}
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
export function isAuthSessionExpiredError(error: unknown): boolean {
|
export function isAuthSessionExpiredError(error: unknown): boolean {
|
||||||
return error instanceof ApiClientError && error.status === 401
|
return error instanceof ApiClientError && error.status === 401
|
||||||
}
|
}
|
||||||
@@ -250,7 +258,7 @@ export async function refreshStoredAuthSession(
|
|||||||
|
|
||||||
expireStoredSession()
|
expireStoredSession()
|
||||||
}
|
}
|
||||||
throw error
|
throw markAuthErrorHandled(error)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
storedRefreshPromise = null
|
storedRefreshPromise = null
|
||||||
@@ -893,7 +901,7 @@ async function subscribeSSE<TEvent extends SSEEventShape>(
|
|||||||
if (isAuthSessionExpiredError(error)) {
|
if (isAuthSessionExpiredError(error)) {
|
||||||
expireStoredSession()
|
expireStoredSession()
|
||||||
}
|
}
|
||||||
throw error
|
throw markAuthErrorHandled(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,6 +956,7 @@ async function buildSSERequestError(response: Response): Promise<ApiClientError>
|
|||||||
status: response.status,
|
status: response.status,
|
||||||
detail: payload.detail,
|
detail: payload.detail,
|
||||||
requestId: payload.request_id,
|
requestId: payload.request_id,
|
||||||
|
handled: response.status === 401,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -966,6 +975,7 @@ async function buildSSERequestError(response: Response): Promise<ApiClientError>
|
|||||||
message: 'unknown_error',
|
message: 'unknown_error',
|
||||||
status: response.status,
|
status: response.status,
|
||||||
detail: detail ?? `stream request failed with status ${response.status}`,
|
detail: detail ?? `stream request failed with status ${response.status}`,
|
||||||
|
handled: response.status === 401,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import { ApiClientError } from '@geo/http-client'
|
import { ApiClientError } from '@geo/http-client'
|
||||||
|
|
||||||
|
const authSessionErrorMessages = new Set([
|
||||||
|
'not_authenticated',
|
||||||
|
'missing_bearer_token',
|
||||||
|
'invalid_access_token',
|
||||||
|
'invalid_refresh_token',
|
||||||
|
'refresh_session_expired',
|
||||||
|
'refresh_token_mismatch',
|
||||||
|
'token_revoked',
|
||||||
|
])
|
||||||
|
|
||||||
const errorMessageMap: Record<string, string> = {
|
const errorMessageMap: Record<string, string> = {
|
||||||
invalid_credentials: '邮箱或密码错误',
|
invalid_credentials: '邮箱或密码错误',
|
||||||
login_rate_limited: '登录请求过于频繁',
|
login_rate_limited: '登录请求过于频繁',
|
||||||
@@ -79,7 +89,19 @@ const errorMessageMap: Record<string, string> = {
|
|||||||
unknown_error: '发生未知错误',
|
unknown_error: '发生未知错误',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isHandledAuthError(error: unknown): boolean {
|
||||||
|
return (
|
||||||
|
error instanceof ApiClientError &&
|
||||||
|
error.status === 401 &&
|
||||||
|
(error.handled === true || authSessionErrorMessages.has(error.message))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function formatError(error: unknown): string {
|
export function formatError(error: unknown): string {
|
||||||
|
if (isHandledAuthError(error)) {
|
||||||
|
return '登录已过期,请重新登录'
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof ApiClientError) {
|
if (error instanceof ApiClientError) {
|
||||||
const message = errorMessageMap[error.message] ?? error.message.replaceAll('_', ' ')
|
const message = errorMessageMap[error.message] ?? error.message.replaceAll('_', ' ')
|
||||||
return error.detail ? `${message}: ${error.detail}` : message
|
return error.detail ? `${message}: ${error.detail}` : message
|
||||||
|
|||||||
@@ -46,13 +46,14 @@ import {
|
|||||||
Tree,
|
Tree,
|
||||||
TreeSelect,
|
TreeSelect,
|
||||||
Upload,
|
Upload,
|
||||||
|
message,
|
||||||
} from 'ant-design-vue'
|
} from 'ant-design-vue'
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
import 'ant-design-vue/dist/reset.css'
|
import 'ant-design-vue/dist/reset.css'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import { i18n } from './i18n'
|
import { i18n } from './i18n'
|
||||||
import { subscribeAuthExpired } from './lib/session'
|
import { subscribeAuthExpired, subscribeStoredSession } from './lib/session'
|
||||||
import { router } from './router'
|
import { router } from './router'
|
||||||
import { pinia } from './stores/pinia'
|
import { pinia } from './stores/pinia'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
@@ -68,6 +69,17 @@ const queryClient = new QueryClient({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
let authExpiredNotified = false
|
||||||
|
|
||||||
|
message.config({
|
||||||
|
maxCount: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
subscribeStoredSession((snapshot) => {
|
||||||
|
if (snapshot.accessToken) {
|
||||||
|
authExpiredNotified = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
subscribeAuthExpired(() => {
|
subscribeAuthExpired(() => {
|
||||||
const current = router.currentRoute.value
|
const current = router.currentRoute.value
|
||||||
@@ -75,10 +87,15 @@ subscribeAuthExpired(() => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
void router.replace({
|
if (!authExpiredNotified) {
|
||||||
name: 'login',
|
authExpiredNotified = true
|
||||||
query: current.fullPath ? { redirect: current.fullPath } : undefined,
|
message.destroy()
|
||||||
})
|
message.warning('登录已过期,请重新登录')
|
||||||
|
void router.replace({
|
||||||
|
name: 'login',
|
||||||
|
query: current.fullPath ? { redirect: current.fullPath } : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const IconFont = createFromIconfontCN({
|
const IconFont = createFromIconfontCN({
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export class OpsApiError extends Error {
|
|||||||
code: number
|
code: number
|
||||||
detail?: string
|
detail?: string
|
||||||
requestId?: string
|
requestId?: string
|
||||||
|
handled?: boolean
|
||||||
|
|
||||||
constructor(input: {
|
constructor(input: {
|
||||||
message: string
|
message: string
|
||||||
@@ -14,6 +15,7 @@ export class OpsApiError extends Error {
|
|||||||
status?: number
|
status?: number
|
||||||
detail?: string
|
detail?: string
|
||||||
requestId?: string
|
requestId?: string
|
||||||
|
handled?: boolean
|
||||||
}) {
|
}) {
|
||||||
super(input.message)
|
super(input.message)
|
||||||
this.name = 'OpsApiError'
|
this.name = 'OpsApiError'
|
||||||
@@ -21,6 +23,7 @@ export class OpsApiError extends Error {
|
|||||||
this.status = input.status
|
this.status = input.status
|
||||||
this.detail = input.detail
|
this.detail = input.detail
|
||||||
this.requestId = input.requestId
|
this.requestId = input.requestId
|
||||||
|
this.handled = input.handled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,11 +36,20 @@ interface ApiEnvelope<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let onUnauthorized: (() => void) | null = null
|
let onUnauthorized: (() => void) | null = null
|
||||||
|
let unauthorizedHandled = false
|
||||||
|
|
||||||
export function bindUnauthorizedHandler(handler: () => void): void {
|
export function bindUnauthorizedHandler(handler: () => void): void {
|
||||||
onUnauthorized = handler
|
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({
|
const client: AxiosInstance = axios.create({
|
||||||
baseURL: '/api/ops',
|
baseURL: '/api/ops',
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
@@ -63,10 +75,14 @@ client.interceptors.response.use(
|
|||||||
(error: AxiosError<ApiEnvelope<unknown>>) => {
|
(error: AxiosError<ApiEnvelope<unknown>>) => {
|
||||||
const status = error.response?.status
|
const status = error.response?.status
|
||||||
const payload = error.response?.data
|
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()
|
clearStoredSession()
|
||||||
onUnauthorized?.()
|
if (!unauthorizedHandled) {
|
||||||
|
unauthorizedHandled = true
|
||||||
|
onUnauthorized?.()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
@@ -76,6 +92,7 @@ client.interceptors.response.use(
|
|||||||
status,
|
status,
|
||||||
detail: typeof payload?.detail === 'string' ? payload.detail : undefined,
|
detail: typeof payload?.detail === 'string' ? payload.detail : undefined,
|
||||||
requestId: typeof payload?.request_id === 'string' ? payload.request_id : undefined,
|
requestId: typeof payload?.request_id === 'string' ? payload.request_id : undefined,
|
||||||
|
handled,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { createApp } from 'vue'
|
|||||||
|
|
||||||
import { bindUnauthorizedHandler } from '@/lib/http'
|
import { bindUnauthorizedHandler } from '@/lib/http'
|
||||||
import { router } from '@/router'
|
import { router } from '@/router'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { pinia } from '@/stores/pinia'
|
import { pinia } from '@/stores/pinia'
|
||||||
import 'ant-design-vue/dist/reset.css'
|
import 'ant-design-vue/dist/reset.css'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
@@ -55,8 +56,15 @@ const queryClient = new QueryClient({
|
|||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
message.config({
|
||||||
|
maxCount: 1,
|
||||||
|
})
|
||||||
|
|
||||||
bindUnauthorizedHandler(() => {
|
bindUnauthorizedHandler(() => {
|
||||||
|
const auth = useAuthStore(pinia)
|
||||||
|
auth.logout()
|
||||||
void router.replace({ name: 'login' })
|
void router.replace({ name: 'login' })
|
||||||
|
message.destroy()
|
||||||
message.warning('登录已过期,请重新登录')
|
message.warning('登录已过期,请重新登录')
|
||||||
})
|
})
|
||||||
;[
|
;[
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import { http } from '@/lib/http'
|
import { http, resetUnauthorizedHandling } from '@/lib/http'
|
||||||
import {
|
import {
|
||||||
type OperatorView,
|
type OperatorView,
|
||||||
clearStoredSession,
|
clearStoredSession,
|
||||||
@@ -47,6 +47,7 @@ export const useAuthStore = defineStore('ops-auth', () => {
|
|||||||
password: payload.password,
|
password: payload.password,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
resetUnauthorizedHandling()
|
||||||
accessToken.value = result.access_token
|
accessToken.value = result.access_token
|
||||||
expiresAt.value = result.expires_at
|
expiresAt.value = result.expires_at
|
||||||
operator.value = result.operator
|
operator.value = result.operator
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ import { message } from 'ant-design-vue'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
import { OpsApiError, http } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
import { http } from '@/lib/http'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
interface AccountRow {
|
interface AccountRow {
|
||||||
@@ -211,7 +212,7 @@ async function reload() {
|
|||||||
rows.value = result.items
|
rows.value = result.items
|
||||||
total.value = result.total
|
total.value = result.total
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -266,7 +267,7 @@ async function submitCreate() {
|
|||||||
resetCreate()
|
resetCreate()
|
||||||
void reload()
|
void reload()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
createLoading.value = false
|
createLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -297,7 +298,7 @@ async function submitResetPassword() {
|
|||||||
message.success('重置成功')
|
message.success('重置成功')
|
||||||
resetOpen.value = false
|
resetOpen.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
resetLoading.value = false
|
resetLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -310,7 +311,7 @@ async function toggleStatus(row: AccountRow) {
|
|||||||
message.success(next === 'active' ? '已启用' : '已停用')
|
message.success(next === 'active' ? '已启用' : '已停用')
|
||||||
void reload()
|
void reload()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -302,7 +302,8 @@ import { message } from 'ant-design-vue'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
import { OpsApiError, http } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
import { http } from '@/lib/http'
|
||||||
|
|
||||||
interface AdminUserRow {
|
interface AdminUserRow {
|
||||||
id: number
|
id: number
|
||||||
@@ -447,7 +448,7 @@ async function reload() {
|
|||||||
rows.value = result.items
|
rows.value = result.items
|
||||||
total.value = result.total
|
total.value = result.total
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -462,7 +463,7 @@ async function loadMeta() {
|
|||||||
plans.value = planResult
|
plans.value = planResult
|
||||||
roles.value = roleResult
|
roles.value = roleResult
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,7 +527,7 @@ async function submitCreate() {
|
|||||||
resetCreate()
|
resetCreate()
|
||||||
void reload()
|
void reload()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
createLoading.value = false
|
createLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -608,7 +609,7 @@ async function submitEdit() {
|
|||||||
editOpen.value = false
|
editOpen.value = false
|
||||||
void reload()
|
void reload()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
editLoading.value = false
|
editLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -624,7 +625,7 @@ async function resetPlanUsage(row: AdminUserRow) {
|
|||||||
rows.value = rows.value.map((item) => (item.id === row.id ? result.user : item))
|
rows.value = rows.value.map((item) => (item.id === row.id ? result.user : item))
|
||||||
void reload()
|
void reload()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
resetUsageLoadingId.value = null
|
resetUsageLoadingId.value = null
|
||||||
}
|
}
|
||||||
@@ -644,7 +645,7 @@ async function resetLoginLock(row: AdminUserRow) {
|
|||||||
message.warning('未发现登录锁 Redis key,请检查 ops-api 与 admin-web 使用的 Redis 地址和 DB 是否一致')
|
message.warning('未发现登录锁 Redis key,请检查 ops-api 与 admin-web 使用的 Redis 地址和 DB 是否一致')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
resetLoginLockLoadingId.value = null
|
resetLoginLockLoadingId.value = null
|
||||||
}
|
}
|
||||||
@@ -675,7 +676,7 @@ async function submitResetPassword() {
|
|||||||
message.success('密码已重置')
|
message.success('密码已重置')
|
||||||
resetOpen.value = false
|
resetOpen.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
resetLoading.value = false
|
resetLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -711,7 +712,7 @@ async function submitKol() {
|
|||||||
kolOpen.value = false
|
kolOpen.value = false
|
||||||
void reload()
|
void reload()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
kolLoading.value = false
|
kolLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -724,7 +725,7 @@ async function toggleStatus(row: AdminUserRow) {
|
|||||||
message.success(next === 'active' ? '已启用' : '已停用')
|
message.success(next === 'active' ? '已启用' : '已停用')
|
||||||
void reload()
|
void reload()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ import { message } from 'ant-design-vue'
|
|||||||
import dayjs, { type Dayjs } from 'dayjs'
|
import dayjs, { type Dayjs } from 'dayjs'
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { OpsApiError, http } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
import { http } from '@/lib/http'
|
||||||
|
|
||||||
interface AuditRow {
|
interface AuditRow {
|
||||||
id: number
|
id: number
|
||||||
@@ -161,7 +162,7 @@ async function reload() {
|
|||||||
rows.value = result.items
|
rows.value = result.items
|
||||||
total.value = result.total
|
total.value = result.total
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,8 @@ import { message } from 'ant-design-vue'
|
|||||||
import dayjs, { type Dayjs } from 'dayjs'
|
import dayjs, { type Dayjs } from 'dayjs'
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
import { OpsApiError, http } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
import { http } from '@/lib/http'
|
||||||
|
|
||||||
interface JobSummary {
|
interface JobSummary {
|
||||||
uid: string
|
uid: string
|
||||||
@@ -429,11 +430,7 @@ function formatJSON(value: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showError(error: unknown) {
|
function showError(error: unknown) {
|
||||||
if (error instanceof OpsApiError) {
|
showOpsError(error)
|
||||||
message.error(error.detail || error.message)
|
|
||||||
} else {
|
|
||||||
message.error('请求失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -232,7 +232,8 @@ import { message } from 'ant-design-vue'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
import { OpsApiError, http } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
import { http } from '@/lib/http'
|
||||||
|
|
||||||
interface KolSubscriptionRow {
|
interface KolSubscriptionRow {
|
||||||
id: number
|
id: number
|
||||||
@@ -349,7 +350,7 @@ async function reload() {
|
|||||||
rows.value = result.items
|
rows.value = result.items
|
||||||
total.value = result.total
|
total.value = result.total
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -363,7 +364,7 @@ async function loadPackages(keyword = '') {
|
|||||||
size: 50,
|
size: 50,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
packageLoading.value = false
|
packageLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -437,7 +438,7 @@ async function submitManualBind() {
|
|||||||
page.value = 1
|
page.value = 1
|
||||||
void reload()
|
void reload()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
manualBindLoading.value = false
|
manualBindLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -472,7 +473,7 @@ async function submitApprove() {
|
|||||||
approveOpen.value = false
|
approveOpen.value = false
|
||||||
void reload()
|
void reload()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
approveLoading.value = false
|
approveLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -487,7 +488,7 @@ async function revoke(row: KolSubscriptionRow) {
|
|||||||
message.success('订阅已撤销')
|
message.success('订阅已撤销')
|
||||||
void reload()
|
void reload()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) message.error(error.detail || error.message)
|
showOpsError(error)
|
||||||
} finally {
|
} finally {
|
||||||
revokeLoadingId.value = null
|
revokeLoadingId.value = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,8 @@
|
|||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { reactive, ref } from 'vue'
|
import { reactive, ref } from 'vue'
|
||||||
|
|
||||||
import { OpsApiError, http } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
import { http } from '@/lib/http'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
@@ -71,9 +72,7 @@ async function onSubmit() {
|
|||||||
// router redirect handled by 401 interceptor or next route guard
|
// router redirect handled by 401 interceptor or next route guard
|
||||||
if (typeof window !== 'undefined') window.location.href = '/login'
|
if (typeof window !== 'undefined') window.location.href = '/login'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpsApiError) {
|
showOpsError(error)
|
||||||
message.error(error.detail || error.message)
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,8 @@ import { message } from 'ant-design-vue'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
import { OpsApiError, http } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
import { http } from '@/lib/http'
|
||||||
|
|
||||||
interface SiteDomainMappingRow {
|
interface SiteDomainMappingRow {
|
||||||
id: number
|
id: number
|
||||||
@@ -342,9 +343,7 @@ function formatDate(value: string | null | undefined): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleError(error: unknown) {
|
function handleError(error: unknown) {
|
||||||
if (error instanceof OpsApiError) {
|
showOpsError(error)
|
||||||
message.error(error.detail || error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ import {
|
|||||||
platformLabel,
|
platformLabel,
|
||||||
} from '@/lib/compliance-display'
|
} from '@/lib/compliance-display'
|
||||||
import { complianceApi, type OpsComplianceDictionary } from '@/lib/compliance'
|
import { complianceApi, type OpsComplianceDictionary } from '@/lib/compliance'
|
||||||
import { OpsApiError } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -220,9 +220,7 @@ function formatDate(value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showError(error: unknown) {
|
function showError(error: unknown) {
|
||||||
if (error instanceof OpsApiError) {
|
showOpsError(error)
|
||||||
message.error(error.detail || error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ import {
|
|||||||
type OpsComplianceDictionary,
|
type OpsComplianceDictionary,
|
||||||
type OpsComplianceTerm,
|
type OpsComplianceTerm,
|
||||||
} from '@/lib/compliance'
|
} from '@/lib/compliance'
|
||||||
import { OpsApiError } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
|
||||||
type MatchType = 'exact' | 'regex'
|
type MatchType = 'exact' | 'regex'
|
||||||
|
|
||||||
@@ -514,9 +514,7 @@ function resetTermForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showError(error: unknown) {
|
function showError(error: unknown) {
|
||||||
if (error instanceof OpsApiError) {
|
showOpsError(error)
|
||||||
message.error(error.detail || error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ import {
|
|||||||
sourceLabel,
|
sourceLabel,
|
||||||
} from '@/lib/compliance-display'
|
} from '@/lib/compliance-display'
|
||||||
import { complianceApi } from '@/lib/compliance'
|
import { complianceApi } from '@/lib/compliance'
|
||||||
import { OpsApiError } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -195,9 +195,7 @@ function formatDate(value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showError(error: unknown) {
|
function showError(error: unknown) {
|
||||||
if (error instanceof OpsApiError) {
|
showOpsError(error)
|
||||||
message.error(error.detail || error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -89,7 +89,6 @@
|
|||||||
import type { ComplianceManualReview } from '@geo/shared-types'
|
import type { ComplianceManualReview } from '@geo/shared-types'
|
||||||
import { ReloadOutlined } from '@ant-design/icons-vue'
|
import { ReloadOutlined } from '@ant-design/icons-vue'
|
||||||
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue'
|
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue'
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
@@ -101,7 +100,7 @@ import {
|
|||||||
reviewStatusLabel,
|
reviewStatusLabel,
|
||||||
} from '@/lib/compliance-display'
|
} from '@/lib/compliance-display'
|
||||||
import { complianceApi } from '@/lib/compliance'
|
import { complianceApi } from '@/lib/compliance'
|
||||||
import { OpsApiError } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -166,9 +165,7 @@ function formatDate(value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showError(error: unknown) {
|
function showError(error: unknown) {
|
||||||
if (error instanceof OpsApiError) {
|
showOpsError(error)
|
||||||
message.error(error.detail || error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ import {
|
|||||||
type CompliancePolicy,
|
type CompliancePolicy,
|
||||||
type OpsComplianceDictionary,
|
type OpsComplianceDictionary,
|
||||||
} from '@/lib/compliance'
|
} from '@/lib/compliance'
|
||||||
import { OpsApiError } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
|
||||||
const policy = ref<CompliancePolicy | null>(null)
|
const policy = ref<CompliancePolicy | null>(null)
|
||||||
const dictionaries = ref<OpsComplianceDictionary[]>([])
|
const dictionaries = ref<OpsComplianceDictionary[]>([])
|
||||||
@@ -220,9 +220,7 @@ function formatDate(value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showError(error: unknown) {
|
function showError(error: unknown) {
|
||||||
if (error instanceof OpsApiError) {
|
showOpsError(error)
|
||||||
message.error(error.detail || error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -99,7 +99,6 @@
|
|||||||
import type { ComplianceCheckResult } from '@geo/shared-types'
|
import type { ComplianceCheckResult } from '@geo/shared-types'
|
||||||
import { ReloadOutlined } from '@ant-design/icons-vue'
|
import { ReloadOutlined } from '@ant-design/icons-vue'
|
||||||
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue'
|
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue'
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
@@ -114,7 +113,7 @@ import {
|
|||||||
reviewStatusLabel,
|
reviewStatusLabel,
|
||||||
} from '@/lib/compliance-display'
|
} from '@/lib/compliance-display'
|
||||||
import { complianceApi } from '@/lib/compliance'
|
import { complianceApi } from '@/lib/compliance'
|
||||||
import { OpsApiError } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
|
||||||
const rows = ref<ComplianceCheckResult[]>([])
|
const rows = ref<ComplianceCheckResult[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -187,9 +186,7 @@ function formatDate(value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showError(error: unknown) {
|
function showError(error: unknown) {
|
||||||
if (error instanceof OpsApiError) {
|
showOpsError(error)
|
||||||
message.error(error.detail || error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -74,12 +74,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ReloadOutlined } from '@ant-design/icons-vue'
|
import { ReloadOutlined } from '@ant-design/icons-vue'
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { levelColor, levelLabel, sourceLabel } from '@/lib/compliance-display'
|
import { levelColor, levelLabel, sourceLabel } from '@/lib/compliance-display'
|
||||||
import { complianceApi, type ComplianceStats } from '@/lib/compliance'
|
import { complianceApi, type ComplianceStats } from '@/lib/compliance'
|
||||||
import { OpsApiError } from '@/lib/http'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
|
||||||
const stats = ref<ComplianceStats | null>(null)
|
const stats = ref<ComplianceStats | null>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -105,9 +104,7 @@ function toRows(value?: Record<string, number>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showError(error: unknown) {
|
function showError(error: unknown) {
|
||||||
if (error instanceof OpsApiError) {
|
showOpsError(error)
|
||||||
message.error(error.detail || error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export class ApiClientError extends Error {
|
|||||||
code: number
|
code: number
|
||||||
detail?: string
|
detail?: string
|
||||||
requestId?: string
|
requestId?: string
|
||||||
|
handled?: boolean
|
||||||
|
|
||||||
constructor(input: {
|
constructor(input: {
|
||||||
message: string
|
message: string
|
||||||
@@ -33,6 +34,7 @@ export class ApiClientError extends Error {
|
|||||||
status?: number
|
status?: number
|
||||||
detail?: string
|
detail?: string
|
||||||
requestId?: string
|
requestId?: string
|
||||||
|
handled?: boolean
|
||||||
}) {
|
}) {
|
||||||
super(input.message)
|
super(input.message)
|
||||||
this.name = 'ApiClientError'
|
this.name = 'ApiClientError'
|
||||||
@@ -40,6 +42,7 @@ export class ApiClientError extends Error {
|
|||||||
this.status = input.status
|
this.status = input.status
|
||||||
this.detail = input.detail
|
this.detail = input.detail
|
||||||
this.requestId = input.requestId
|
this.requestId = input.requestId
|
||||||
|
this.handled = input.handled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +81,11 @@ function unwrapEnvelope<T>(response: { data: ApiEnvelope<T> }): T {
|
|||||||
return response.data.data
|
return response.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markHandled(error: ApiClientError): ApiClientError {
|
||||||
|
error.handled = true
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
export function createApiClient(options: CreateApiClientOptions = {}): ApiClient {
|
export function createApiClient(options: CreateApiClientOptions = {}): ApiClient {
|
||||||
const client = axios.create({
|
const client = axios.create({
|
||||||
baseURL: options.baseURL ?? '',
|
baseURL: options.baseURL ?? '',
|
||||||
@@ -136,12 +144,14 @@ export function createApiClient(options: CreateApiClientOptions = {}): ApiClient
|
|||||||
const normalizedRefreshError = normalizeError(refreshError)
|
const normalizedRefreshError = normalizeError(refreshError)
|
||||||
if (normalizedRefreshError.status === 401) {
|
if (normalizedRefreshError.status === 401) {
|
||||||
auth.clearTokens()
|
auth.clearTokens()
|
||||||
|
throw markHandled(normalizedRefreshError)
|
||||||
}
|
}
|
||||||
throw normalizedRefreshError
|
throw normalizedRefreshError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.clearTokens()
|
auth.clearTokens()
|
||||||
|
throw markHandled(normalizeError(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
throw normalizeError(error)
|
throw normalizeError(error)
|
||||||
|
|||||||
Reference in New Issue
Block a user