From 8456ba90c8b24c2734a34e4d3ab3fc0b6adadfff Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 31 Mar 2026 23:45:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E6=9D=83=E9=99=90=E7=9A=84=E8=BF=9B=E4=BF=AE=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=9B=9E=E5=88=B0=E7=99=BB=E5=BD=95=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/request.ts | 56 +++++++++++++++++++++++++-------------- src/main.ts | 14 +++++++++- src/stores/user.ts | 6 +++++ src/views/TradeDetail.vue | 2 -- 4 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/api/request.ts b/src/api/request.ts index 1dad2f5..a2d6503 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -1,5 +1,36 @@ import { i18n } from '@/plugins/i18n' +export type HttpUnauthorizedHandler = () => void + +let httpUnauthorizedHandler: HttpUnauthorizedHandler | null = null + +/** + * 注册 HTTP 401 时的处理(如清除登录态并跳转登录页)。 + * 在 main.ts 中 app.use(pinia)、app.use(router) 之后调用,避免循环依赖。 + */ +export function setHttpUnauthorizedHandler(handler: HttpUnauthorizedHandler | null) { + httpUnauthorizedHandler = handler +} + +function notifyHttpUnauthorized() { + try { + httpUnauthorizedHandler?.() + } catch (e) { + console.error('[request] httpUnauthorizedHandler failed', e) + } +} + +async function parseJsonBody(res: Response): Promise { + if (res.status === 401) { + notifyHttpUnauthorized() + throw new Error(`HTTP 401: ${res.statusText}`) + } + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`) + } + return res.json() as Promise +} + /** 请求基础 URL,默认 https://api.xtrader.vip,可通过环境变量 VITE_API_BASE_URL 覆盖 */ export const BASE_URL = (import.meta as { env?: { VITE_API_BASE_URL?: string } }).env?.VITE_API_BASE_URL ?? @@ -77,10 +108,7 @@ export async function get( ...config?.headers, } const res = await fetch(url.toString(), { method: 'GET', headers }) - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`) - } - return res.json() as Promise + return parseJsonBody(res) } /** @@ -102,10 +130,7 @@ export async function post( headers, body: body !== undefined ? JSON.stringify(body) : undefined, }) - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`) - } - return res.json() as Promise + return parseJsonBody(res) } /** @@ -129,10 +154,7 @@ export async function uploadFile( headers, body: form, }) - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`) - } - return res.json() as Promise + return parseJsonBody(res) } /** @@ -154,10 +176,7 @@ export async function put( headers, body: body !== undefined ? JSON.stringify(body) : undefined, }) - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`) - } - return res.json() as Promise + return parseJsonBody(res) } /** @@ -179,8 +198,5 @@ export async function httpDelete( headers, body: body !== undefined ? JSON.stringify(body) : undefined, }) - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`) - } - return res.json() as Promise + return parseJsonBody(res) } diff --git a/src/main.ts b/src/main.ts index 1fc1111..0fe3f7c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,11 +5,23 @@ import App from './App.vue' import router from './router' import vuetify from './plugins/vuetify' import { i18n } from './plugins/i18n' +import { setHttpUnauthorizedHandler } from './api/request' +import { useUserStore } from './stores/user' const app = createApp(App) -app.use(createPinia()) +const pinia = createPinia() +app.use(pinia) app.use(router) + +setHttpUnauthorizedHandler(() => { + const userStore = useUserStore(pinia) + userStore.clearLocalSession() + if (router.currentRoute.value.name !== 'login') { + router.replace({ name: 'login' }) + } +}) + app.use(i18n) app.use(vuetify) diff --git a/src/stores/user.ts b/src/stores/user.ts index 0a003b0..b347eaa 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -165,6 +165,11 @@ export const useUserStore = defineStore('user', () => { // 忽略失败,仍执行本地登出 } } + clearLocalSession() + } + + /** 仅清除本地登录态(如 HTTP 401),不请求服务端黑名单 */ + function clearLocalSession() { disconnectUserSocket() token.value = '' user.value = null @@ -235,6 +240,7 @@ export const useUserStore = defineStore('user', () => { balance, setUser, logout, + clearLocalSession, getAuthHeaders, fetchUsdcBalance, fetchUserInfo, diff --git a/src/views/TradeDetail.vue b/src/views/TradeDetail.vue index 6d4940f..3988bf4 100644 --- a/src/views/TradeDetail.vue +++ b/src/views/TradeDetail.vue @@ -1454,8 +1454,6 @@ async function updateChartData() { ensureChartSeries() setChartData(data.value) nextTick(() => scheduleCryptoScrollToRealtime()) - cryptoWsUnsubscribe?.() - cryptoWsUnsubscribe = null cryptoWsUnsubscribe = subscribeCryptoRealtime(sym, applyCryptoRealtimePoint) } finally { if (gen === cryptoLoadGeneration) {