浏览代码

feat(auth): 实现token失效时自动关闭子窗口并跳转首页- 添加closeAllWindows IPC方法用于关闭所有子窗口
- 在token失效时调用closeAllWindows关闭子窗口
- token失效时清空用户信息并跳转到首页
- 优化syncAfterLogin函数,支持无token时跳过同步
- 修复登录后同步数据时未传入token的问题
- 增强loginOut函数,支持关闭子窗口和跳转首页- 优化实时预览弹窗关闭逻辑,支持无token时直接关闭

panqiuyao 2 月之前
父节点
当前提交
80a77500c2

+ 29 - 0
electron/controller/utils.js

@@ -93,6 +93,35 @@ class UtilsController extends Controller {
     return filePaths
     return filePaths
   }
   }
 
 
+  /**
+   * 关闭所有子窗口
+   */
+  closeAllWindows() {
+    try {
+      // 获取所有窗口
+      const windows = this.app.electron;
+
+      // 关闭除主窗口外的所有窗口
+      for (const [id, window] of Object.entries(windows)) {
+
+        if (!['mainWindow','extra'].includes(id)) { // 保留主窗口
+          try {
+            window.close();
+            delete this.app.electron[id];
+          } catch (error) {
+            console.error(`关闭窗口 ${id} 失败:`, error);
+          }
+        }
+      }
+
+      console.log('所有子窗口已关闭');
+      return { success: true, message: '所有子窗口已关闭' };
+    } catch (error) {
+      console.error('关闭所有窗口失败:', error);
+      return { success: false, message: '关闭所有窗口失败: ' + error.message };
+    }
+  }
+
   async openImage(
   async openImage(
       optiops= {
       optiops= {
         title:"选择图片",
         title:"选择图片",

+ 9 - 1
frontend/src/apis/setting.ts

@@ -177,9 +177,17 @@ export async function saveDeviceConfig(data){
 }
 }
 
 
 // 登录后同步数据
 // 登录后同步数据
-export async function syncAfterLogin(token: string) {
+export async function syncAfterLogin() {
     try {
     try {
         const clientStore = client();
         const clientStore = client();
+        const tokenInfoStore = tokenInfo();
+        const token = tokenInfoStore.getToken;
+        
+        if (!token) {
+            console.warn('没有token,跳过数据同步');
+            return;
+        }
+        
         // 同步系统配置
         // 同步系统配置
         await clientStore.ipc.invoke(icpList.setting.syncSysConfigs, {
         await clientStore.ipc.invoke(icpList.setting.syncSysConfigs, {
             token: token
             token: token

+ 2 - 2
frontend/src/components/login/index.vue

@@ -213,7 +213,7 @@ const handleLogin = async () => {
     default:
     default:
       await  useUserInfoStore.getInfo()
       await  useUserInfoStore.getInfo()
       // 登录成功后同步数据
       // 登录成功后同步数据
-      await syncAfterLogin(res.data.token)
+      await syncAfterLogin()
       useUserInfoStore.updateLoginShow(false)
       useUserInfoStore.updateLoginShow(false)
       setTimeout(()=>{
       setTimeout(()=>{
       //  window.location.reload()
       //  window.location.reload()
@@ -255,7 +255,7 @@ async function toggleCompany() {
   })
   })
   await  useUserInfoStore.getInfo()
   await  useUserInfoStore.getInfo()
   // 选择公司后也需要同步数据
   // 选择公司后也需要同步数据
-  await syncAfterLogin(res.data.token)
+  await syncAfterLogin()
   useUserInfoStore.updateLoginShow(false)
   useUserInfoStore.updateLoginShow(false)
   setTimeout(()=>{
   setTimeout(()=>{
     // window.location.reload()
     // window.location.reload()

+ 29 - 6
frontend/src/stores/modules/user.ts

@@ -2,6 +2,8 @@ import { defineStore } from 'pinia';
 import { ref, computed } from 'vue';
 import { ref, computed } from 'vue';
 import { getUserInfo, login } from '@/apis/user';
 import { getUserInfo, login } from '@/apis/user';
 import tokenInfo from '@/stores/modules/token';
 import tokenInfo from '@/stores/modules/token';
+import client from '@/stores/modules/client';
+import { useRouter } from 'vue-router';
 
 
 
 
 export const useUserInfo = defineStore('userInfo', () => {
 export const useUserInfo = defineStore('userInfo', () => {
@@ -63,18 +65,39 @@ export const useUserInfo = defineStore('userInfo', () => {
 
 
 
 
   /**
   /**
-   * 执行用户登录操作。
+   * 执行用户退出登录操作。
    *
    *
-   * @param {any} data - 登录所需的用户凭据
-   * @returns {Promise<any>} 登录接口返回的结果。
-   * @throws {Error} 如果登录失败,抛出错误。
+   * @param {any} data - 退出登录所需的参数
+   * @returns {Promise<any>} 退出登录的结果。
+   * @throws {Error} 如果退出登录失败,抛出错误。
    */
    */
   const loginOut = async (data: any) => {
   const loginOut = async (data: any) => {
     try {
     try {
+      // 关闭所有子窗口
+      try {
+        const clientStore = client();
+        await clientStore.ipc.invoke('controller.utils.closeAllWindows');
+        console.log('所有子窗口已关闭');
+      } catch (error) {
+        console.error('关闭子窗口失败:', error);
+        // 关闭窗口失败不影响退出登录流程
+      }
+
+      // 清空用户信息
       await updateToken(''); // 更新登录令牌
       await updateToken(''); // 更新登录令牌
-      await updateUserInfo({})
+      await updateUserInfo({});
+
+      // 跳转到首页
+      try {
+
+        window.location.href = '/'
+        console.log('已跳转到首页');
+      } catch (error) {
+        console.error('跳转到首页失败:', error);
+        // 跳转失败不影响退出登录流程
+      }
     } catch (error) {
     } catch (error) {
-      console.error('登录失败:', error);
+      console.error('退出登录失败:', error);
       throw error;
       throw error;
     }
     }
   };
   };

+ 40 - 2
frontend/src/utils/http.ts

@@ -4,6 +4,7 @@ import  tokenInfo  from '@/stores/modules/token';
 import useUserInfo from "@/stores/modules/user";
 import useUserInfo from "@/stores/modules/user";
 import pinia from "@/stores/index";
 import pinia from "@/stores/index";
 import ENV_CONFIG from "@/config.json";
 import ENV_CONFIG from "@/config.json";
+import client from '@/stores/modules/client';
 
 
 // 加载动画的并发管理
 // 加载动画的并发管理
 const activeRequests = new Set<string>();
 const activeRequests = new Set<string>();
@@ -21,6 +22,41 @@ function loadingClose(requestId: string) {
 }
 }
 
 
 /**
 /**
+ * 处理token失效
+ * 关闭所有子窗口,清空用户信息,跳转到首页
+ */
+async function handleTokenExpiry() {
+    try {
+        // 关闭所有子窗口
+        try {
+            const clientStore = client();
+            await clientStore.ipc.invoke('controller.utils.closeAllWindows');
+            console.log('Token失效:所有子窗口已关闭');
+        } catch (error) {
+            console.error('Token失效:关闭子窗口失败:', error);
+        }
+
+        // 清空用户信息
+        const useUserInfoStore = useUserInfo();
+        await useUserInfoStore.updateToken('');
+        await useUserInfoStore.updateUserInfo({});
+
+        // 跳转到首页
+        try {
+            window.location.href = '/'
+            console.log('Token失效:已跳转到首页');
+        } catch (error) {
+            console.error('Token失效:跳转到首页失败:', error);
+        }
+
+        // 显示登录弹窗
+        useUserInfoStore.updateLoginShow(true);
+    } catch (error) {
+        console.error('Token失效处理失败:', error);
+    }
+}
+
+/**
  * 创建一个axios实例,用于发送HTTP请求
  * 创建一个axios实例,用于发送HTTP请求
  * 配置了请求拦截器和响应拦截器,支持加载动画和错误提示
  * 配置了请求拦截器和响应拦截器,支持加载动画和错误提示
  */
  */
@@ -96,7 +132,8 @@ service.interceptors.response.use(
                         type: 'error',
                         type: 'error',
                         duration: 3 * 1000,
                         duration: 3 * 1000,
                     });
                     });
-                    useUserInfoStore.updateLoginShow(true)
+                    // 处理token失效
+                    handleTokenExpiry();
                     break;
                     break;
                 default:
                 default:
                     if (response.config.showErrorMessage) {
                     if (response.config.showErrorMessage) {
@@ -136,7 +173,8 @@ service.interceptors.response.use(
                 switch (error.response.status) {
                 switch (error.response.status) {
                     case 400: errMessage = '请求错误(400)'; break;
                     case 400: errMessage = '请求错误(400)'; break;
                     case 401: errMessage = '登录状态已失效,请重新登录';
                     case 401: errMessage = '登录状态已失效,请重新登录';
-                    useUserInfoStore.updateLoginShow(true)
+                    // 处理token失效
+                    handleTokenExpiry();
                      break;
                      break;
                     case 403: errMessage = '拒绝访问(403)'; break;
                     case 403: errMessage = '拒绝访问(403)'; break;
                     case 404: errMessage = '请求出错(404)'; break;
                     case 404: errMessage = '请求出错(404)'; break;

+ 2 - 1
frontend/src/utils/ipc.ts

@@ -20,7 +20,8 @@ const icpList = {
         openDirectory:"controller.utils.openDirectory",
         openDirectory:"controller.utils.openDirectory",
         openImage:"controller.utils.openImage",
         openImage:"controller.utils.openImage",
         getAppConfig:"controller.utils.getAppConfig",
         getAppConfig:"controller.utils.getAppConfig",
-        openFile:"controller.utils.openFile"
+        openFile:"controller.utils.openFile",
+        closeAllWindows: 'controller.utils.closeAllWindows'
     },
     },
     setting:{
     setting:{
         getDeviceConfigDetail: 'controller.setting.getDeviceConfigDetail',
         getDeviceConfigDetail: 'controller.setting.getDeviceConfigDetail',

+ 1 - 1
frontend/src/views/Home/index.vue

@@ -97,7 +97,7 @@ const checkHealth = async () => {
           try {
           try {
             // 导入同步函数
             // 导入同步函数
             const { syncAfterLogin } = await import('@/apis/setting');
             const { syncAfterLogin } = await import('@/apis/setting');
-            await syncAfterLogin(token);
+            await syncAfterLogin();
             console.log('健康检查后数据同步成功');
             console.log('健康检查后数据同步成功');
           } catch (syncError) {
           } catch (syncError) {
             console.error('健康检查后数据同步失败:', syncError);
             console.error('健康检查后数据同步失败:', syncError);

+ 29 - 0
frontend/src/views/Setting/components/action_config.vue

@@ -95,9 +95,11 @@ import { ElMessage, ElMessageBox } from 'element-plus';
 import { Rank, Warning } from '@element-plus/icons-vue';
 import { Rank, Warning } from '@element-plus/icons-vue';
 import client from "@/stores/modules/client";
 import client from "@/stores/modules/client";
 import icpList from '@/utils/ipc';
 import icpList from '@/utils/ipc';
+import tokenInfo from '@/stores/modules/token';
 const clientStore = client();
 const clientStore = client();
 import socket from "@/stores/modules/socket";
 import socket from "@/stores/modules/socket";
 const socketStore = socket(); // WebSocket状态管理实例
 const socketStore = socket(); // WebSocket状态管理实例
+const tokenInfoStore = tokenInfo();
 
 
 import  { getTopTabs, getDeviceConfigs,setLeftRightConfig,restConfig,setTabName,delDviceConfig } from '@/apis/setting'
 import  { getTopTabs, getDeviceConfigs,setLeftRightConfig,restConfig,setTabName,delDviceConfig } from '@/apis/setting'
 
 
@@ -133,6 +135,24 @@ onMounted(()=>{
 
 
 const handleBeforeUnload = (e)=>{
 const handleBeforeUnload = (e)=>{
   if(dialogVisible.value){
   if(dialogVisible.value){
+    // 没有token时,直接关闭实时预览,不显示提示
+    dialogVisible.value = false;
+    // 调用hideVideo逻辑
+    hideVideo();
+    return;
+
+
+    // 检查是否有token
+    const token = tokenInfoStore.getToken;
+    if (!token || token.trim() === '') {
+      // 没有token时,直接关闭实时预览,不显示提示
+      dialogVisible.value = false;
+      // 调用hideVideo逻辑
+      hideVideo();
+      return;
+    }
+
+    // 有token时,显示提示阻止关闭
     e.preventDefault();
     e.preventDefault();
     const message = '您已打开实时预览弹出框,请先取消或者保存后,关闭编辑弹出框,后再关闭此窗口';
     const message = '您已打开实时预览弹出框,请先取消或者保存后,关闭编辑弹出框,后再关闭此窗口';
     e.returnValue = message; // 标准方式
     e.returnValue = message; // 标准方式
@@ -141,6 +161,15 @@ const handleBeforeUnload = (e)=>{
   }
   }
 
 
 }
 }
+
+// 添加hideVideo函数
+function hideVideo(){
+  clientStore.ipc.removeAllListeners(icpList.camera.PreviewHide);
+  clientStore.ipc.send(icpList.camera.PreviewHide);
+  clientStore.ipc.on(icpList.camera.PreviewHide, async () => {
+    // 这里可以添加清理逻辑,比如清除定时器等
+  })
+}
 /**
 /**
  * 监听topsTab变化,获取对应标签页的设备配置列表。
  * 监听topsTab变化,获取对应标签页的设备配置列表。
  */
  */