Selaa lähdekoodia

feat: add system exit functionality with cross-platform support

- Add preload.js with secure system operation APIs
- Create ExitModal.tsx component with logout/shutdown options
- Implement cross-platform system commands in main.js
- Update SystemZone.tsx and electron-builder.json configuration
- Update build output .build/h5_for_electron.build.win.x64.js
- Configure ESLint to ignore main.js and preload.js files

Files: preload.js, src/components/ExitModal.tsx, main.js, src/layouts/SystemZone.tsx, electron-builder.json, .eslintignore, eslint.config.mjs

close #58
sw 2 viikkoa sitten
vanhempi
commit
a80f79e8fa

+ 2 - 2
.build/h5_for_electron.build.win.x64.js

@@ -29,8 +29,8 @@ function getGhToken() {
   }
 }
 
-const TARO_API_URL = 'http://localhost:6001'; // 远程地址,这里写死,要做成部署后可配置
-const TARO_MQTT_URL ='ws://localhost:8083/mqtt';
+const TARO_API_URL = 'http://192.168.110.112:6001'; // 远程地址,这里写死,要做成部署后可配置
+const TARO_MQTT_URL ='ws://192.168.110.112:8083/mqtt';
 const rootDir = path.join(__dirname, '..');          // 项目根目录
 
 

+ 3 - 1
.eslintignore

@@ -1 +1,3 @@
-cypress/
+cypress/
+main.js
+preload.js

+ 1 - 1
electron-builder.json

@@ -4,7 +4,7 @@
   "electronVersion": "36.2.1",
   "asar": true,
   "compression": "store",
-  "files": ["main.js", "src/log/**/*", "!node_modules/**/*"],
+  "files": ["main.js", "src/log/**/*", "!node_modules/**/*", "preload.js"],
   "extraFiles": [
     {
       "from": "dist/h5",

+ 1 - 1
eslint.config.mjs

@@ -16,7 +16,7 @@ export default tseslint.config(
 
     prettierConfig,
     {
-        ignores: ['babel.config.js', 'node_modules/', 'postcss.config.js', 'tailwind.config.js','**/app.config.ts', '.build/', 'src/log/logger.js'],
+        ignores: ['babel.config.js', 'node_modules/', 'postcss.config.js', 'tailwind.config.js','**/app.config.ts', '.build/', 'src/log/logger.js', 'main.js', 'preload.js'],
     },
     {
         files: ['**/*.ts', '**/*.tsx'],

+ 107 - 4
main.js

@@ -1,12 +1,15 @@
 // main.js  (ESM 版本)
-import { app, BrowserWindow, Menu, ipcMain } from 'electron';
+import { app, BrowserWindow, Menu, ipcMain, shell } from 'electron';
 import { fileURLToPath } from 'url';
 import { dirname, join } from 'path';
+import { exec } from 'child_process';
+import { promisify } from 'util';
 import { writeLog } from './src/log/log-writer.js';
 
 // -------------- 构造 ESM 版 __dirname --------------
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = dirname(__filename);
+const execAsync = promisify(exec);
 
 // -------------- 创建窗口 --------------
 function createWindow() {
@@ -17,8 +20,9 @@ function createWindow() {
     frame: !isMac,
     titleBarStyle: isMac ? 'hiddenInset' : 'default',
     webPreferences: {
-      nodeIntegration: true,
-      contextIsolation: false,
+      nodeIntegration: false,
+      contextIsolation: true,
+      preload: join(__dirname, 'preload.js'),
     },
   });
 
@@ -50,7 +54,106 @@ app.on('activate', () => {
   if (BrowserWindow.getAllWindows().length === 0) createWindow();
 });
 
+// -------------- 系统操作函数 --------------
+const getSystemCommands = () => {
+  const platform = process.platform;
+  
+  switch (platform) {
+    case 'win32': // Windows
+      return {
+        logout: 'shutdown /l',
+        shutdown: 'shutdown /s /t 0'
+      };
+    case 'darwin': // macOS
+      return {
+        logout: 'sudo launchctl bootout gui/$(id -u)',
+        shutdown: 'sudo shutdown -h now'
+      };
+    case 'linux': // Linux
+      return {
+        logout: 'loginctl terminate-user $USER',
+        shutdown: 'systemctl poweroff'
+      };
+    default:
+      return {
+        logout: null,
+        shutdown: null
+      };
+  }
+};
+
+const executeSystemCommand = async (command, requiresAdmin = false) => {
+  try {
+    if (!command) {
+      throw new Error('当前平台不支持该操作');
+    }
+    
+    // Windows平台的权限检查
+    if (process.platform === 'win32' && requiresAdmin) {
+      // 检查是否以管理员身份运行
+      try {
+        await execAsync('net session', { timeout: 5000 });
+      } catch (adminCheckError) {
+        writeLog('warn', '操作可能需要管理员权限');
+        return { 
+          success: false, 
+          error: '该操作需要管理员权限,请以管理员身份运行程序后重试',
+          requiresAdmin: true
+        };
+      }
+    }
+    
+    const { stdout, stderr } = await execAsync(command, { timeout: 10000 });
+    writeLog('info', `系统命令执行成功: ${command}`);
+    
+    if (stderr && stderr.trim()) {
+      writeLog('warn', `系统命令有警告信息: ${stderr}`);
+    }
+    
+    return { success: true, stdout, stderr };
+  } catch (error) {
+    let errorMessage = error.message;
+    
+    // 特定错误类型的处理
+    if (error.code === 'EACCES') {
+      errorMessage = '权限不足,请以管理员身份运行程序';
+    } else if (error.code === 'ENOENT') {
+      errorMessage = '系统命令不存在或路径错误';
+    } else if (error.code === 'ETIMEDOUT') {
+      errorMessage = '操作超时,请重试';
+    }
+    
+    writeLog('error', `系统命令执行失败: ${command}, 错误: ${errorMessage}`);
+    return { success: false, error: errorMessage };
+  }
+};
+
 // -------------- IPC --------------
 ipcMain.handle('write-log', async (_, level, msg) => {
   writeLog(level, msg);
-});
+});
+
+// 退出应用
+ipcMain.handle('exit-close', async () => {
+  writeLog('info', '用户选择关闭应用程序');
+  app.quit();
+  return { success: true };
+});
+
+// 注销用户
+ipcMain.handle('exit-logout', async () => {
+  writeLog('info', '用户选择注销系统');
+  const commands = getSystemCommands();
+  // 注销通常需要管理员权限(Windows平台)
+  const requiresAdmin = process.platform === 'win32';
+  return await executeSystemCommand(commands.logout, requiresAdmin);
+});
+
+// 关机
+ipcMain.handle('exit-shutdown', async () => {
+  writeLog('info', '用户选择关机');
+  const commands = getSystemCommands();
+  // 关机通常需要管理员权限
+  const requiresAdmin = true;
+  return await executeSystemCommand(commands.shutdown, requiresAdmin);
+});

+ 13 - 0
preload.js

@@ -0,0 +1,13 @@
+// preload.js
+const { contextBridge, ipcRenderer } = require('electron');
+
+// 暴露安全的 API 给渲染进程
+contextBridge.exposeInMainWorld('electronAPI', {
+  // 系统退出相关操作
+  exitApp: () => ipcRenderer.invoke('exit-close'),
+  logoutUser: () => ipcRenderer.invoke('exit-logout'), 
+  shutdownSystem: () => ipcRenderer.invoke('exit-shutdown'),
+  
+  // 日志功能(保留原有功能)
+  writeLog: (level, msg) => ipcRenderer.invoke('write-log', level, msg)
+});

+ 176 - 0
src/components/ExitModal.tsx

@@ -0,0 +1,176 @@
+import React from 'react';
+import { Modal, Button, Space, Typography, message } from 'antd';
+import {
+  PoweroffOutlined,
+  LogoutOutlined,
+  CloseOutlined,
+} from '@ant-design/icons';
+
+const { Text } = Typography;
+
+interface ExitModalProps {
+  visible: boolean;
+  onClose: () => void;
+}
+
+declare global {
+  interface Window {
+    electronAPI: {
+      exitApp: () => Promise<{ success: boolean }>;
+      logoutUser: () => Promise<{
+        success: boolean;
+        error?: string;
+        requiresAdmin?: boolean;
+      }>;
+      shutdownSystem: () => Promise<{
+        success: boolean;
+        error?: string;
+        requiresAdmin?: boolean;
+      }>;
+    };
+  }
+}
+
+const ExitModal: React.FC<ExitModalProps> = ({ visible, onClose }) => {
+  const handleExit = async (type: 'close' | 'logout' | 'shutdown') => {
+    try {
+      let result;
+      let actionName = '';
+
+      // 添加操作确认
+      const isDangerousOperation = type === 'shutdown' || type === 'logout';
+      if (isDangerousOperation) {
+        // 这里可以添加二次确认逻辑,暂时省略,直接执行
+      }
+
+      switch (type) {
+        case 'close':
+          actionName = '关闭程序';
+          result = await window.electronAPI.exitApp();
+          break;
+        case 'logout':
+          actionName = '注销用户';
+          result = await window.electronAPI.logoutUser();
+          break;
+        case 'shutdown':
+          actionName = '关机';
+          result = await window.electronAPI.shutdownSystem();
+          break;
+        default:
+          return;
+      }
+
+      if (result.success) {
+        message.success(`${actionName}操作已执行`);
+        if (type === 'close') {
+          // 关闭程序操作会直接退出,不需要关闭弹框
+          return;
+        }
+      } else {
+        // 处理不同类型的错误
+        if (result.requiresAdmin) {
+          message.warning({
+            content: `${actionName}需要管理员权限,请以管理员身份运行程序后重试`,
+            duration: 5,
+          });
+        } else {
+          message.error({
+            content: `${actionName}失败: ${result.error || '未知错误'}`,
+            duration: 4,
+          });
+        }
+      }
+    } catch (error) {
+      console.error('系统操作失败:', error);
+      message.error({
+        content: '操作失败,请检查网络连接后重试',
+        duration: 3,
+      });
+    }
+
+    onClose();
+  };
+
+  return (
+    <Modal
+      title="选择退出方式"
+      open={visible}
+      onCancel={onClose}
+      footer={null}
+      centered
+      width={400}
+    >
+      <div style={{ textAlign: 'center', padding: '20px 0' }}>
+        <Text type="secondary" style={{ marginBottom: 24, display: 'block' }}>
+          请选择您希望执行的操作
+        </Text>
+
+        <Space direction="vertical" size="large" style={{ width: '100%' }}>
+          <Button
+            type="default"
+            size="large"
+            icon={<CloseOutlined />}
+            onClick={() => handleExit('close')}
+            style={{
+              width: '100%',
+              height: 48,
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+            }}
+          >
+            关闭程序
+          </Button>
+
+          <Button
+            type="default"
+            size="large"
+            icon={<LogoutOutlined />}
+            onClick={() => handleExit('logout')}
+            style={{
+              width: '100%',
+              height: 48,
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+            }}
+          >
+            注销用户
+          </Button>
+
+          <Button
+            type="default"
+            size="large"
+            icon={<PoweroffOutlined />}
+            onClick={() => handleExit('shutdown')}
+            danger
+            style={{
+              width: '100%',
+              height: 48,
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+            }}
+          >
+            关机
+          </Button>
+
+          <Button
+            type="default"
+            size="large"
+            onClick={onClose}
+            style={{
+              width: '100%',
+              height: 48,
+              marginTop: 16,
+            }}
+          >
+            取消
+          </Button>
+        </Space>
+      </div>
+    </Modal>
+  );
+};
+
+export default ExitModal;

+ 36 - 1
src/layouts/SystemZone.tsx

@@ -1,4 +1,4 @@
-import React, { forwardRef } from 'react';
+import React, { forwardRef, useState } from 'react';
 import { Space, Row } from 'antd';
 import MeButton from '../pages/security/components/MeButton';
 import { useSelector } from 'react-redux';
@@ -6,6 +6,7 @@ import { RootState } from '@/states/store';
 import { isLoggedIn } from '../states/user_info';
 import Icon from '@/components/Icon';
 import { IconButton } from '@/components/IconButton';
+import ExitModal from '@/components/ExitModal';
 
 interface SystemZoneProps {
   onMenuClick?: (key: string) => void;
@@ -17,6 +18,17 @@ const SystemZone = forwardRef<HTMLDivElement, SystemZoneProps>(
     const username = useSelector((state: RootState) => state.userInfo.name);
     const avatarUrl = useSelector((state: RootState) => state.userInfo.avatar);
 
+    // 退出弹框状态管理
+    const [exitModalVisible, setExitModalVisible] = useState(false);
+
+    const handleExitClick = () => {
+      setExitModalVisible(true);
+    };
+
+    const handleExitModalClose = () => {
+      setExitModalVisible(false);
+    };
+
     return (
       <Row
         ref={ref}
@@ -64,7 +76,30 @@ const SystemZone = forwardRef<HTMLDivElement, SystemZoneProps>(
             username={login ? username : '未登录'}
             onClick={() => onMenuClick?.('me')}
           />
+
+          <IconButton
+            icon={
+              <Icon
+                module="module-common"
+                name="btn_3DCam_AIView"
+                userId="user-A"
+                theme="default"
+                size="2x"
+                state="normal"
+              />
+            }
+            iconPlace="left"
+            iconSize={32}
+            type="primary"
+            style={{ padding: '4px 16px' }}
+            onClick={handleExitClick}
+          >
+            退出
+          </IconButton>
         </Space>
+
+        {/* 退出选择弹框 */}
+        <ExitModal visible={exitModalVisible} onClose={handleExitModalClose} />
       </Row>
     );
   }