diff --git a/web/src/utils/axios.ts b/web/src/utils/axios.ts index bd3cecc..8cd0e54 100644 --- a/web/src/utils/axios.ts +++ b/web/src/utils/axios.ts @@ -34,6 +34,7 @@ function resolveAdminThinkLang(): string { window.requests = [] window.tokenRefreshing = false +window.authRedirecting = false const pendingMap = new Map() const loadingInstance: LoadingInstance = { target: null, @@ -245,20 +246,119 @@ export default createAxios * 处理异常 * @param {*} error */ +/** 解析 axios 错误响应体(可能已是对象或 JSON 字符串) */ +function normalizeAxiosErrorBody(data: unknown): anyObj | null { + if (data === undefined || data === null) { + return null + } + if (typeof data === 'string') { + try { + return JSON.parse(data) as anyObj + } catch { + return null + } + } + if (typeof data === 'object') { + return data as anyObj + } + return null +} + +/** + * 后端未登录时通过 HTTP 303 + body.data.type === 'need login' 提示登录(见 Auth::LOGIN_RESPONSE_CODE)。 + * Axios 将 303 视为失败,不会进入成功拦截器里对 code==303 的分支,需在此与之一致:清 token 并跳登录页。 + */ +function redirectToLoginIfNeedLoginFromHttp303(error: any): boolean { + if (!error?.response || error.response.status !== 303) { + return false + } + const body = normalizeAxiosErrorBody(error.response.data) + if (!body || typeof body.data !== 'object' || body.data === null) { + return false + } + const needType = (body.data as anyObj).type + if (needType !== 'need login') { + return false + } + if (window.authRedirecting) { + return true + } + const isAdminAppFlag = isAdminApp() + const loginRouteName = isAdminAppFlag ? 'adminLogin' : 'userLogin' + if (router.currentRoute.value.name === loginRouteName) { + return true + } + window.authRedirecting = true + const adminInfo = useAdminInfo() + const userInfo = useUserInfo() + if (isAdminAppFlag) { + adminInfo.removeToken() + router.replace({ name: loginRouteName }).finally(() => { + window.authRedirecting = false + }) + } else { + userInfo.removeToken() + const to = router.currentRoute.value.fullPath + router.replace({ name: loginRouteName, query: to ? { to } : {} }).finally(() => { + window.authRedirecting = false + }) + } + return true +} + function httpErrorStatusHandle(error: any) { // 处理被取消的请求 if (axios.isCancel(error)) return console.error(i18n.global.t('axios.Automatic cancellation due to duplicate request:') + error.message) + if (redirectToLoginIfNeedLoginFromHttp303(error)) { + return + } let message = '' if (error && error.response) { switch (error.response.status) { case 302: message = i18n.global.t('axios.Interface redirected!') break + case 303: { + const body303 = normalizeAxiosErrorBody(error.response.data) + const serverMsg303 = body303 && typeof body303.msg === 'string' ? body303.msg : '' + message = serverMsg303 !== '' ? serverMsg303 : i18n.global.t('axios.Interface redirected!') + break + } case 400: message = i18n.global.t('axios.Incorrect parameter!') break case 401: message = i18n.global.t('axios.You do not have permission to operate!') + { + const data = error.response.data + const serverMsg = data && typeof data.msg === 'string' ? data.msg : '' + if (serverMsg) { + message = serverMsg + } + + const isAdminAppFlag = isAdminApp() + const loginRouteName = isAdminAppFlag ? 'adminLogin' : 'userLogin' + if (!window.authRedirecting && router.currentRoute.value.name !== loginRouteName) { + window.authRedirecting = true + + const adminInfo = useAdminInfo() + const userInfo = useUserInfo() + if (isAdminAppFlag) { + adminInfo.removeToken() + router.replace({ name: loginRouteName }).finally(() => { + window.authRedirecting = false + }) + } else { + userInfo.removeToken() + const to = router.currentRoute.value.fullPath + router + .replace({ name: loginRouteName, query: to ? { to } : {} }) + .finally(() => { + window.authRedirecting = false + }) + } + } + } break case 403: message = i18n.global.t('axios.You do not have permission to operate!') @@ -271,6 +371,49 @@ function httpErrorStatusHandle(error: any) { break case 409: message = i18n.global.t('axios.The same data already exists in the system!') + { + const data = error.response.data + const serverMsg = data && typeof data.msg === 'string' ? data.msg : '' + const authExpiredHints = [ + 'Token expiration', + 'token expiration', + '登录态过期', + '请重新登录', + 'Session expired', + 'session expired', + 'please login again', + 'sila log masuk semula', + ] + const looksLikeAuthExpired = + serverMsg !== '' && authExpiredHints.some((hint) => serverMsg.toLowerCase().includes(hint.toLowerCase())) + + if (looksLikeAuthExpired) { + message = serverMsg + + const isAdminAppFlag = isAdminApp() + const loginRouteName = isAdminAppFlag ? 'adminLogin' : 'userLogin' + if (!window.authRedirecting && router.currentRoute.value.name !== loginRouteName) { + window.authRedirecting = true + + const adminInfo = useAdminInfo() + const userInfo = useUserInfo() + if (isAdminAppFlag) { + adminInfo.removeToken() + router.replace({ name: loginRouteName }).finally(() => { + window.authRedirecting = false + }) + } else { + userInfo.removeToken() + const to = router.currentRoute.value.fullPath + router + .replace({ name: loginRouteName, query: to ? { to } : {} }) + .finally(() => { + window.authRedirecting = false + }) + } + } + } + } break case 500: message = i18n.global.t('axios.Server internal error!') diff --git a/web/types/global.d.ts b/web/types/global.d.ts index 85c9e97..da00bff 100644 --- a/web/types/global.d.ts +++ b/web/types/global.d.ts @@ -3,6 +3,7 @@ interface Window { lazy: number unique: number tokenRefreshing: boolean + authRedirecting: boolean requests: Function[] eventSource: EventSource loadLangHandle: Record