sunshibo 3 дней назад
Родитель
Сommit
4aa4512e4e

+ 47 - 0
src/api/helper/handleAuthExpired.ts

@@ -0,0 +1,47 @@
+import { ElMessage } from "element-plus";
+import router from "@/routers";
+import { LOGIN_URL } from "@/config";
+import { useUserStore } from "@/stores/modules/user";
+import { ResultEnum } from "@/enums/httpEnum";
+
+let dedupeAuthExpired = false;
+
+/** 登录成功后调用,避免踢下线去重标志影响新会话 */
+export function resetAuthExpiredDedupe() {
+  dedupeAuthExpired = false;
+}
+
+export function isAuthExpiredCode(code: unknown): boolean {
+  return code == ResultEnum.OVERDUE || code == ResultEnum.KICK_OUT;
+}
+
+/**
+ * 登录失效 / 账号在别处登录:清 token、跳转登录;仅首次执行副作用。
+ * 异地挤下线(666)使用整页跳转,避免仍停留在业务页;401 仍走路由 + 提示。
+ * 多个接口同时失败时其余请求静默 reject。
+ */
+export function applyAuthExpiredSideEffects(msg?: string, code?: unknown): void {
+  if (dedupeAuthExpired) return;
+  dedupeAuthExpired = true;
+  const userStore = useUserStore();
+  userStore.setToken("");
+
+  const tip = msg || "登录已失效";
+  const isKickOut = code == ResultEnum.KICK_OUT;
+
+  if (isKickOut) {
+    const href = router.resolve({ path: LOGIN_URL }).href;
+    window.location.replace(href);
+    window.setTimeout(() => {
+      dedupeAuthExpired = false;
+    }, 2000);
+    return;
+  }
+
+  ElMessage.error(tip);
+  void router.replace(LOGIN_URL).finally(() => {
+    window.setTimeout(() => {
+      dedupeAuthExpired = false;
+    }, 1000);
+  });
+}

+ 7 - 12
src/api/index.ts

@@ -1,6 +1,5 @@
 import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from "axios";
 import { showFullScreenLoading, tryHideFullScreenLoading } from "@/components/Loading/fullScreen";
-import { LOGIN_URL } from "@/config";
 import { ElMessage } from "element-plus";
 import { ResultData } from "@/api/interface";
 import { ResultEnum } from "@/enums/httpEnum";
@@ -11,6 +10,7 @@ import { localGet } from "@/utils";
 import router from "@/routers";
 import { cryptoUtil } from "@/utils/crypto";
 import { cryptoStrategy } from "@/utils/cryptoStrategy";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
   loading?: boolean;
@@ -77,7 +77,6 @@ class RequestHttp {
       (response: AxiosResponse & { config: CustomAxiosRequestConfig }) => {
         const { data, config, headers } = response;
 
-        const userStore = useUserStore();
         axiosCanceler.removePending(config);
         config.loading && tryHideFullScreenLoading();
 
@@ -94,11 +93,9 @@ class RequestHttp {
         }
 
         const processedData = response.data;
-        // 登录失效
-        if (processedData.code == ResultEnum.OVERDUE) {
-          userStore.setToken("");
-          router.replace(LOGIN_URL);
-          ElMessage.error(processedData.msg);
+        // 登录失效 / 账号在别处登录
+        if (isAuthExpiredCode(processedData.code)) {
+          applyAuthExpiredSideEffects(processedData.msg, processedData.code);
           return Promise.reject(processedData);
         }
         // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错)
@@ -118,7 +115,7 @@ class RequestHttp {
         if (error.message.indexOf("timeout") !== -1) ElMessage.error("请求超时!请您稍后重试");
         if (error.message.indexOf("Network Error") !== -1) ElMessage.error("网络错误!请您稍后重试");
         // 根据服务器响应的错误状态码,做不同的处理
-        if (response.data.message) {
+        if (response?.data?.message) {
           ElMessage.error(response.data.message);
         } else {
           if (response) checkStatus(response.status);
@@ -185,10 +182,8 @@ class RequestHttp {
           try {
             const response = JSON.parse(xhr.responseText);
             // 统一处理响应,与 axios 拦截器保持一致
-            if (response.code == ResultEnum.OVERDUE) {
-              userStore.setToken("");
-              router.replace(LOGIN_URL);
-              ElMessage.error(response.msg);
+            if (isAuthExpiredCode(response.code)) {
+              applyAuthExpiredSideEffects(response.msg, response.code);
               reject(response);
               return;
             }

+ 4 - 7
src/api/indexApi.ts

@@ -1,6 +1,5 @@
 import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from "axios";
 import { showFullScreenLoading, tryHideFullScreenLoading } from "@/components/Loading/fullScreen";
-import { LOGIN_URL } from "@/config";
 import { ElMessage } from "element-plus";
 import { ResultData } from "@/api/interface";
 import { ResultEnum } from "@/enums/httpEnum";
@@ -10,6 +9,7 @@ import { useUserStore } from "@/stores/modules/user";
 import router from "@/routers";
 import { cryptoUtil } from "@/utils/crypto";
 import { cryptoStrategy } from "@/utils/cryptoStrategy";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
   loading?: boolean;
@@ -77,7 +77,6 @@ class RequestHttp {
     this.service.interceptors.response.use(
       (response: AxiosResponse & { config: CustomAxiosRequestConfig }) => {
         const { data, config, headers } = response;
-        const userStore = useUserStore();
         axiosCanceler.removePending(config);
         config.loading && tryHideFullScreenLoading();
 
@@ -95,11 +94,9 @@ class RequestHttp {
 
         const processedData = response.data;
 
-        // 登录失效
-        if (processedData.code == ResultEnum.OVERDUE) {
-          userStore.setToken("");
-          router.replace(LOGIN_URL);
-          ElMessage.error(processedData.msg);
+        // 登录失效 / 账号在别处登录
+        if (isAuthExpiredCode(processedData.code)) {
+          applyAuthExpiredSideEffects(processedData.msg, processedData.code);
           return Promise.reject(processedData);
         }
         // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错)

+ 4 - 6
src/api/indexLogin.ts

@@ -1,6 +1,5 @@
 import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from "axios";
 import { showFullScreenLoading, tryHideFullScreenLoading } from "@/components/Loading/fullScreen";
-import { LOGIN_URL } from "@/config";
 import { ElMessage } from "element-plus";
 import { ResultData } from "@/api/interface";
 import { ResultEnum } from "@/enums/httpEnum";
@@ -10,6 +9,7 @@ import { useUserStore } from "@/stores/modules/user";
 import router from "@/routers";
 import { cryptoUtil } from "@/utils/crypto";
 import { cryptoStrategy } from "@/utils/cryptoStrategy";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
   loading?: boolean;
@@ -94,11 +94,9 @@ class RequestHttp {
 
         const processedData = response.data;
 
-        // 登录失效
-        if (processedData.code == ResultEnum.OVERDUE) {
-          userStore.setToken("");
-          router.replace(LOGIN_URL);
-          ElMessage.error(processedData.msg);
+        // 登录失效 / 账号在别处登录
+        if (isAuthExpiredCode(processedData.code)) {
+          applyAuthExpiredSideEffects(processedData.msg, processedData.code);
           return Promise.reject(processedData);
         }
         // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错)

+ 7 - 12
src/api/indexStore.ts

@@ -1,6 +1,5 @@
 import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from "axios";
 import { showFullScreenLoading, tryHideFullScreenLoading } from "@/components/Loading/fullScreen";
-import { LOGIN_URL } from "@/config";
 import { ElMessage } from "element-plus";
 import { ResultData } from "@/api/interface";
 import { ResultEnum } from "@/enums/httpEnum";
@@ -11,6 +10,7 @@ import { localGet } from "@/utils";
 import router from "@/routers";
 import { cryptoUtil } from "@/utils/crypto";
 import { cryptoStrategy } from "@/utils/cryptoStrategy";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
   loading?: boolean;
@@ -77,7 +77,6 @@ class RequestHttp {
       (response: AxiosResponse & { config: CustomAxiosRequestConfig }) => {
         const { data, config, headers } = response;
 
-        const userStore = useUserStore();
         axiosCanceler.removePending(config);
         config.loading && tryHideFullScreenLoading();
 
@@ -95,11 +94,9 @@ class RequestHttp {
 
         const processedData = response.data;
 
-        // 登录失效
-        if (processedData.code == ResultEnum.OVERDUE) {
-          userStore.setToken("");
-          router.replace(LOGIN_URL);
-          ElMessage.error(processedData.msg);
+        // 登录失效 / 账号在别处登录
+        if (isAuthExpiredCode(processedData.code)) {
+          applyAuthExpiredSideEffects(processedData.msg, processedData.code);
           return Promise.reject(processedData);
         }
         // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错)
@@ -117,7 +114,7 @@ class RequestHttp {
         if (error.message.indexOf("timeout") !== -1) ElMessage.error("请求超时!请您稍后重试");
         if (error.message.indexOf("Network Error") !== -1) ElMessage.error("网络错误!请您稍后重试");
         // 根据服务器响应的错误状态码,做不同的处理
-        if (response.data.message) {
+        if (response?.data?.message) {
           ElMessage.error(response.data.message);
         } else {
           if (response) checkStatus(response.status);
@@ -184,10 +181,8 @@ class RequestHttp {
           try {
             const response = JSON.parse(xhr.responseText);
             // 统一处理响应,与 axios 拦截器保持一致
-            if (response.code == ResultEnum.OVERDUE) {
-              userStore.setToken("");
-              router.replace(LOGIN_URL);
-              ElMessage.error(response.msg);
+            if (isAuthExpiredCode(response.code)) {
+              applyAuthExpiredSideEffects(response.msg, response.code);
               reject(response);
               return;
             }

+ 3 - 7
src/api/modules/homeEntry.ts

@@ -5,8 +5,7 @@ import http from "@/api";
 import { ResultEnum } from "@/enums/httpEnum";
 import { useUserStore } from "@/stores/modules/user";
 import { ElMessage } from "element-plus";
-import { LOGIN_URL } from "@/config";
-import router from "@/routers";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 const httpStore = axios.create({
   baseURL: import.meta.env.VITE_API_URL_STORE as string,
@@ -26,11 +25,8 @@ httpStore.interceptors.request.use(
 httpStore.interceptors.response.use(
   response => {
     const data = response.data;
-    const userStore = useUserStore();
-    if (data.code == ResultEnum.OVERDUE) {
-      userStore.setToken("");
-      router.replace(LOGIN_URL);
-      ElMessage.error(data.msg);
+    if (isAuthExpiredCode(data.code)) {
+      applyAuthExpiredSideEffects(data.msg, data.code);
       return Promise.reject(data);
     }
     if (data.code && data.code !== ResultEnum.SUCCESS) {

+ 3 - 8
src/api/modules/operationManagement.ts

@@ -7,8 +7,7 @@ import axios from "axios";
 import { ResultEnum } from "@/enums/httpEnum";
 import { useUserStore } from "@/stores/modules/user";
 import { ElMessage } from "element-plus";
-import { LOGIN_URL } from "@/config";
-import router from "@/routers";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 // 创建专门用于 STORE API 的 axios 实例(单例)
 const storeAxiosInstance = axios.create({
@@ -35,13 +34,9 @@ storeAxiosInstance.interceptors.request.use(
 storeAxiosInstance.interceptors.response.use(
   (response: any) => {
     const { data } = response;
-    const userStore = useUserStore();
 
-    // 登录失效
-    if (data.code == ResultEnum.OVERDUE) {
-      userStore.setToken("");
-      router.replace(LOGIN_URL);
-      ElMessage.error(data.msg);
+    if (isAuthExpiredCode(data.code)) {
+      applyAuthExpiredSideEffects(data.msg, data.code);
       return Promise.reject(data);
     }
     // 全局错误信息拦截

+ 3 - 7
src/api/modules/performance.ts

@@ -8,8 +8,7 @@ import { localGet } from "@/utils";
 import { ResultEnum } from "@/enums/httpEnum";
 import { useUserStore } from "@/stores/modules/user";
 import { ElMessage } from "element-plus";
-import { LOGIN_URL } from "@/config";
-import router from "@/routers";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 const performanceAxios = axios.create({
   baseURL: import.meta.env.VITE_API_URL_STORE as string,
@@ -31,11 +30,8 @@ performanceAxios.interceptors.request.use(
 performanceAxios.interceptors.response.use(
   response => {
     const data = response.data;
-    const userStore = useUserStore();
-    if (data.code == ResultEnum.OVERDUE) {
-      userStore.setToken("");
-      router.replace(LOGIN_URL);
-      ElMessage.error(data.msg);
+    if (isAuthExpiredCode(data.code)) {
+      applyAuthExpiredSideEffects(data.msg, data.code);
       return Promise.reject(data);
     }
     if (data.code && data.code !== ResultEnum.SUCCESS) {

+ 3 - 9
src/api/modules/priceList.ts

@@ -4,8 +4,7 @@ import { localGet } from "@/utils";
 import { ResultEnum } from "@/enums/httpEnum";
 import { useUserStore } from "@/stores/modules/user";
 import { ElMessage } from "element-plus";
-import { LOGIN_URL } from "@/config";
-import router from "@/routers";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 // 创建专门用于价目表(STORE)接口的 axios 实例,前缀使用 alienStore
 const priceListAxios = axios.create({
@@ -30,13 +29,8 @@ priceListAxios.interceptors.request.use(
 priceListAxios.interceptors.response.use(
   response => {
     const data = response.data;
-    const userStore = useUserStore();
-
-    // 登录失效
-    if (data.code == ResultEnum.OVERDUE) {
-      userStore.setToken("");
-      router.replace(LOGIN_URL);
-      ElMessage.error(data.msg);
+    if (isAuthExpiredCode(data.code)) {
+      applyAuthExpiredSideEffects(data.msg, data.code);
       return Promise.reject(data);
     }
 

+ 2 - 0
src/enums/httpEnum.ts

@@ -5,6 +5,8 @@ export enum ResultEnum {
   SUCCESS = 200,
   ERROR = 500,
   OVERDUE = 401,
+  /** 账号在别处登录等业务踢下线 */
+  KICK_OUT = 666,
   TIMEOUT = 300000,
   TYPE = "success"
 }

+ 7 - 2
src/utils/errorHandler.ts

@@ -5,7 +5,11 @@ import { ElNotification } from "element-plus";
  * */
 const errorHandler = (error: any) => {
   // 过滤 HTTP 请求错误
-  if (error.status || error.status == 0) return false;
+  if (error?.status || error?.status === 0) return false;
+  // 业务接口 reject 的响应体(无 stack),不在此重复弹「未知错误」
+  if (error && typeof error === "object" && !(error instanceof Error) && ("msg" in error || "code" in error)) {
+    return false;
+  }
   let errorMap: { [key: string]: string } = {
     InternalError: "Javascript引擎内部错误",
     ReferenceError: "未找到对象",
@@ -16,9 +20,10 @@ const errorHandler = (error: any) => {
     URIError: "URI错误"
   };
   let errorName = errorMap[error.name] || "未知错误";
+  const message = error instanceof Error ? error.message : String(error ?? "");
   ElNotification({
     title: errorName,
-    message: error,
+    message,
     type: "error",
     duration: 3000
   });

+ 2 - 0
src/views/login/components/LoginForm.vue

@@ -97,6 +97,7 @@ import type { ElForm } from "element-plus";
 import md5 from "md5";
 import { aiLogin } from "@/api/indexAi";
 import { localSet } from "@/utils";
+import { resetAuthExpiredDedupe } from "@/api/helper/handleAuthExpired";
 
 const router = useRouter();
 const userStore = useUserStore();
@@ -260,6 +261,7 @@ const handleLogin = async () => {
 
       if (data) {
         userStore.setToken(data.token);
+        resetAuthExpiredDedupe();
         console.log("AI登录");
 
         // 保存用户信息到localStorage,供AI登录使用

+ 2 - 0
src/views/login/index.vue

@@ -612,6 +612,7 @@ import {
 } from "@/api/modules/newLoginApi";
 import { getMerchantByPhone } from "@/api/modules/homeEntry";
 import { localGet, localRemove, localSet } from "@/utils";
+import { resetAuthExpiredDedupe } from "@/api/helper/handleAuthExpired";
 import * as path from "node:path";
 import { checkMenuClickPermission } from "@/utils/permission";
 import { aiLogin } from "@/api/indexAi";
@@ -910,6 +911,7 @@ const handleLogin = async () => {
 
       if (res.data) {
         userStore.setToken(res.data.token);
+        resetAuthExpiredDedupe();
         userStore.setUserInfo(res.data);
         const userInfo = {
           userInfo: res.data,