yichael 5 сар өмнө
parent
commit
0e0b76727b
8 өөрчлөгдсөн 358 нэмэгдсэн , 196 устгасан
  1. 0 178
      NEW-PAGE.BAT
  2. 29 3
      README.md
  3. 278 2
      main.js
  4. 4 2
      preload.cjs
  5. 1 2
      src/App.jsx
  6. 26 2
      src/pages/home.css
  7. 12 2
      src/pages/home.js
  8. 8 5
      src/pages/home.jsx

+ 0 - 178
NEW-PAGE.BAT

@@ -1,178 +0,0 @@
-@echo off
-setlocal EnableDelayedExpansion
-
-rem ensure we run from script directory
-cd /d "%~dp0"
-
-set /p PAGE_NAME="Enter Page Name: "
-for /f "tokens=* delims= " %%i in ("%PAGE_NAME%") do set "PAGE_NAME=%%i"
-if "%PAGE_NAME%"=="" exit /b
-
-echo ========================================
-echo   Create New Page
-echo ========================================
-echo   1. Create Page With Folder
-echo   2. Create Page
-echo ========================================
-set /p MODE="Select mode (1 or 2) [Default: 1]: "
-for /f "tokens=* delims= " %%i in ("%MODE%") do set "MODE=%%i"
-if "%MODE%"=="" set "MODE=1"
-if not "%MODE%"=="1" if not "%MODE%"=="2" (
-    echo Invalid selection!
-    pause
-    exit /b
-)
-
-if "%MODE%"=="1" (
-    set "PAGES_DIR=src\pages"
-    set "PAGE_FOLDER=src\pages\%PAGE_NAME%"
-    set "JSX_FILE=src\pages\%PAGE_NAME%\%PAGE_NAME%.jsx"
-    set "JS_FILE=src\pages\%PAGE_NAME%\%PAGE_NAME%.js"
-    set "CSS_FILE=src\pages\%PAGE_NAME%\%PAGE_NAME%.css"
-) else (
-    set "PAGES_DIR=src\pages"
-    set "PAGE_FOLDER="
-    set "JSX_FILE=src\pages\%PAGE_NAME%.jsx"
-    set "JS_FILE=src\pages\%PAGE_NAME%.js"
-    set "CSS_FILE=src\pages\%PAGE_NAME%.css"
-)
-
-if "%MODE%"=="1" (
-    if exist "%PAGE_FOLDER%" (
-        echo Page %PAGE_NAME% already exists!
-        pause
-        exit /b
-    )
-) else (
-    if exist "%JSX_FILE%" (
-        echo Page %PAGE_NAME% already exists!
-        pause
-        exit /b
-    )
-)
-
-if not exist "%PAGES_DIR%" mkdir "%PAGES_DIR%"
-if "%MODE%"=="1" if not exist "%PAGE_FOLDER%" mkdir "%PAGE_FOLDER%"
-
-(
-echo import './%PAGE_NAME%.css';
-echo import { %PAGE_NAME%Logic } from './%PAGE_NAME%.js';
-echo.
-echo function %PAGE_NAME%^(^) {
-echo   const logic = %PAGE_NAME%Logic^(^);
-echo.
-echo   return ^(
-echo     ^<div className="%PAGE_NAME%-container"^>
-echo       ^<h1^>%PAGE_NAME%^</h1^>
-echo     ^</div^>
-echo   ^);
-echo }
-echo.
-echo export default %PAGE_NAME%;
-) > "%JSX_FILE%"
-
-(
-echo export function %PAGE_NAME%Logic^(^) {
-echo   // business logic placeholder
-echo.
-echo   return {
-echo     // expose data or methods here
-echo   };
-echo }
-) > "%JS_FILE%"
-
-(
-echo .%PAGE_NAME%-container {
-echo   padding: 20px;
-echo }
-) > "%CSS_FILE%"
-
-echo Created: %JSX_FILE%
-echo Created: %JS_FILE%
-echo Created: %CSS_FILE%
-
-set "APP_FILE=src\App.jsx"
-if exist "%APP_FILE%" (
-    call :AddToApp "%MODE%" "%PAGE_NAME%" "%APP_FILE%"
-    echo Added %PAGE_NAME% to App.jsx
-) else (
-    echo Warning: App.jsx not found, skipping...
-)
-
-echo.
-echo ========================================
-echo Page %PAGE_NAME% created successfully!
-echo ========================================
-echo.
-echo Summary:
-if "%MODE%"=="1" (
-    echo   1. Created folder: %PAGE_FOLDER%
-    echo   2. Created files:
-    echo      - %JSX_FILE%
-    echo      - %JS_FILE%
-    echo      - %CSS_FILE%
-    echo   3. Modified file: src\App.jsx
-    echo      - Added import: import %PAGE_NAME% from './pages/%PAGE_NAME%/%PAGE_NAME%';
-    echo      - Added component: ^<%PAGE_NAME% /^>
-) else (
-    echo   1. Created files in src\pages directory:
-    echo      - %JSX_FILE%
-    echo      - %JS_FILE%
-    echo      - %CSS_FILE%
-    echo   2. Modified file: src\App.jsx
-    echo      - Added import: import %PAGE_NAME% from './pages/%PAGE_NAME%';
-    echo      - Added component: ^<%PAGE_NAME% /^>
-)
-
-pause
-exit /b
-
-:AddToApp
-setlocal EnableDelayedExpansion
-set "MODE_VAL=%~1"
-set "PAGE_VAL=%~2"
-set "APP_PATH=%~3"
-
-if not exist "%APP_PATH%" (
-    echo ERROR: File not found: %APP_PATH%
-    exit /b 1
-)
-
-if "%MODE_VAL%"=="1" (
-    set "IMPORT_LINE=import %PAGE_VAL% from './pages/%PAGE_VAL%/%PAGE_VAL%';"
-) else (
-    set "IMPORT_LINE=import %PAGE_VAL% from './pages/%PAGE_VAL%';"
-)
-
-set "NEED_IMPORT=1"
-set "NEED_COMPONENT=1"
-set "TEMP_FILE=%TEMP%\app_temp_%RANDOM%.jsx"
-if exist "%TEMP_FILE%" del "%TEMP_FILE%"
-
-for /f "usebackq delims=" %%a in ("%APP_PATH%") do (
-    set "line=%%a"
-    set "isImport=0"
-    set "isDiv=0"
-
-    if /i "!line:~0,7!"=="import " set "isImport=1"
-    if not "!line:</div>=!"=="!line!" set "isDiv=1"
-
-    >>"%TEMP_FILE%" echo(!line!
-
-    if !isImport! equ 1 if !NEED_IMPORT! equ 1 (
-        >>"%TEMP_FILE%" echo !IMPORT_LINE!
-        set "NEED_IMPORT=0"
-    )
-
-    if !isDiv! equ 1 if !NEED_COMPONENT! equ 1 (
-        >>"%TEMP_FILE%" echo       ^<%PAGE_VAL% /^>
-        set "NEED_COMPONENT=0"
-    )
-)
-
-if !NEED_IMPORT! equ 1 >>"%TEMP_FILE%" echo !IMPORT_LINE!
-if !NEED_COMPONENT! equ 1 >>"%TEMP_FILE%" echo       ^<%PAGE_VAL% /^>
-
-move /y "%TEMP_FILE%" "%APP_PATH%" >nul
-endlocal
-exit /b

+ 29 - 3
README.md

@@ -1,10 +1,36 @@
-框架:Electron + React + Vite
+**一、框架:Electron + React + Vite
 
-## 开发命令
+**二、开发命令
 - `npm run dev`:只启动 Vite 前端开发服务器。
 - `npm run electron`:直接启动 Electron(加载本地文件)。
 - `npm run electron:dev`:同时启动 Vite 开发服务器并在准备好后打开 Electron(推荐开发时使用)。
 - `npm run build`:构建生产版本。
 - `npm run preview`:预览构建后的应用。
 
-提示:开发模式会自动安装并加载 React DevTools。
+
+**三、将页面设置为子页面(条件渲染)
+
+**步骤:**
+
+**1. 修改 `src/pages/Home.jsx`**:
+   - 添加:`import Devices from './Devices/Devices';`
+   - 在 `HomeLogic()` 中获取:`const { showDevices, setShowDevices } = HomeLogic();`
+   - 在 return 中添加:`{showDevices && <Devices />}` 和按钮控制显示/隐藏
+
+**2. 修改 `src/pages/Home.js`**:
+   - 添加:`import { useState } from 'react';`
+   - 在 `HomeLogic` 中添加:`const [showDevices, setShowDevices] = useState(false);`
+   - return 中暴露:`showDevices, setShowDevices`
+
+**3. 修改 `src/App.jsx`**:
+   - 删除 Devices 的 import 和组件使用
+
+**四、响应式适配方案
+
+**推荐组合:媒体查询 + 相对单位 + Flexbox**
+
+- **布局单位**:`rem`、`vw`、`%`
+- **字体大小**:`clamp(最小, 自适应, 最大)`
+- **间距内边距**:`rem` 或 `%`
+- **关键尺寸**:`vw` 或 `%`
+- **布局控制**:Flexbox + 媒体查询断点

+ 278 - 2
main.js

@@ -1,12 +1,33 @@
-import { app, BrowserWindow, session } from 'electron';
+import { app, BrowserWindow, session, ipcMain } from 'electron';
 import { fileURLToPath } from 'url';
 import path from 'path';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+import os from 'os';
+
+const execAsync = promisify(exec);
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
 const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
 
-// Set Content Security Policy
+/**
+ * 设置内容安全策略 (Content Security Policy, CSP)
+ * 
+ * 功能说明:
+ * - 为 Electron 应用设置 CSP 规则,防止 XSS 攻击
+ * - 开发环境:允许 localhost 连接,便于开发调试
+ * - 生产环境:严格限制资源加载,提高安全性
+ * 
+ * CSP 规则说明:
+ * - default-src 'self': 默认只允许同源资源
+ * - script-src: 控制脚本加载源
+ * - style-src: 控制样式表加载源
+ * - connect-src: 控制网络请求源(如 fetch, WebSocket)
+ * - img-src: 控制图片加载源
+ * - font-src: 控制字体加载源
+ * - worker-src: 控制 Web Worker 加载源
+ */
 function setContentSecurityPolicy() {
   session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
     const csp = isDev
@@ -20,6 +41,25 @@ function setContentSecurityPolicy() {
   });
 }
 
+/**
+ * 创建主窗口
+ * 
+ * 功能说明:
+ * - 创建 Electron 应用的主窗口
+ * - 配置窗口大小和 Web 安全设置
+ * - 根据环境加载不同的内容源
+ * 
+ * 窗口配置:
+ * - 宽度: 1200px
+ * - 高度: 800px
+ * - preload: 预加载脚本路径(用于安全地暴露 Node.js API)
+ * - nodeIntegration: false(禁用 Node.js 集成,提高安全性)
+ * - contextIsolation: true(启用上下文隔离,防止 XSS 攻击)
+ * 
+ * 加载逻辑:
+ * - 开发环境: 加载 Vite 开发服务器 (http://localhost:5173) 并打开开发者工具
+ * - 生产环境: 加载构建后的静态文件 (dist/index.html)
+ */
 function createWindow() {
   const mainWindow = new BrowserWindow({
     width: 1200,
@@ -39,6 +79,233 @@ function createWindow() {
   }
 }
 
+/**
+ * 获取本机局域网 IP 地址
+ * 
+ * 功能说明:
+ * - 遍历所有网络接口,查找第一个非内部的 IPv4 地址
+ * - 用于确定局域网网段,以便扫描同网段的设备
+ * 
+ * 查找逻辑:
+ * - 遍历所有网络接口(如以太网、Wi-Fi 等)
+ * - 过滤条件:IPv4 地址 && 非内部地址(非 127.0.0.1)
+ * - 返回找到的第一个符合条件的 IP 地址
+ * 
+ * @returns {string} 本机局域网 IP 地址,如果未找到则返回默认值 '192.168.1.1'
+ * 
+ * @example
+ * // 如果本机 IP 是 192.168.2.35,则返回 '192.168.2.35'
+ * const localIP = getLocalIP(); // '192.168.2.35'
+ */
+function getLocalIP() {
+  const interfaces = os.networkInterfaces();
+  for (const name of Object.keys(interfaces)) {
+    for (const iface of interfaces[name]) {
+      if (iface.family === 'IPv4' && !iface.internal) {
+        return iface.address;
+      }
+    }
+  }
+  return '192.168.1.1';
+}
+
+/**
+ * 扫描局域网内端口 5555 的 ADB 设备
+ * 
+ * 功能说明:
+ * - 获取本机 IP 地址,确定局域网网段(如 192.168.2.x)
+ * - 扫描该网段内所有可能的 IP 地址(1-254)
+ * - 对每个 IP 尝试连接 ADB 端口 5555
+ * - 使用并行扫描提高效率,每批同时扫描 50 个 IP
+ * 
+ * 扫描策略:
+ * - 根据本机 IP 确定网段(如 192.168.2.35 → 网段 192.168.2.x)
+ * - 扫描范围:192.168.2.1 到 192.168.2.254
+ * - 每批处理 50 个 IP,避免同时发起过多连接
+ * - 每个连接超时时间:300ms(快速失败,提高扫描速度)
+ * - 忽略连接失败的错误(大部分 IP 不会有设备)
+ * 
+ * 性能优化:
+ * - 使用 Promise.all 并行执行,大幅提升扫描速度
+ * - 分批处理避免系统资源耗尽
+ * - 扫描完成后等待 500ms,确保所有连接已建立
+ * 
+ * @returns {Promise<void>} 扫描完成(不返回结果,结果通过 getADBDevices 获取)
+ * 
+ * @example
+ * // 如果本机 IP 是 192.168.2.35
+ * // 会扫描 192.168.2.1:5555 到 192.168.2.254:5555
+ * await scanADBDevices();
+ */
+async function scanADBDevices() {
+  const localIP = getLocalIP();
+  const ipParts = localIP.split('.');
+  const baseIP = `${ipParts[0]}.${ipParts[1]}.${ipParts[2]}`;
+
+  // 并行扫描,每批 50 个 IP
+  const batchSize = 100;
+  const promises = [];
+
+  for (let i = 1; i <= 254; i += batchSize) {
+    const batch = [];
+    for (let j = i; j < i + batchSize && j <= 254; j++) {
+      const ip = `${baseIP}.${j}`;
+      batch.push(
+        execAsync(`adb connect ${ip}:5555`, { timeout: 300 })
+          .catch(() => {}) // 忽略连接失败
+      );
+    }
+    promises.push(Promise.all(batch));
+  }
+
+  await Promise.all(promises);
+  // 等待连接建立
+  await new Promise(resolve => setTimeout(resolve, 500));
+}
+
+/**
+ * 获取已连接的 ADB 设备列表
+ * 
+ * 功能说明:
+ * - 执行 `adb devices` 命令获取当前所有已连接的设备
+ * - 解析命令输出,提取设备 ID 和状态
+ * - 只返回状态为 'device' 的设备(已授权且可用的设备)
+ * 
+ * 命令输出格式:
+ * ```
+ * List of devices attached
+ * 192.168.2.5:5555    device
+ * 3764756281ZZZZZ     device
+ * ```
+ * 
+ * 解析逻辑:
+ * - 跳过第一行标题("List of devices attached")
+ * - 每行按空白字符分割,提取设备 ID 和状态
+ * - 只保留状态为 'device' 的设备(排除 'offline', 'unauthorized' 等)
+ * 
+ * 返回数据格式:
+ * ```javascript
+ * [
+ *   { id: '192.168.2.5:5555', status: 'device' },
+ *   { id: '3764756281ZZZZZ', status: 'device' }
+ * ]
+ * ```
+ * 
+ * @returns {Promise<Array<{id: string, status: string}>>} 设备列表数组
+ * - id: 设备标识符(IP:端口 或 USB 序列号)
+ * - status: 设备状态(通常为 'device')
+ * 
+ * @example
+ * const devices = await getADBDevices();
+ * // [{ id: '192.168.2.5:5555', status: 'device' }]
+ */
+async function getADBDevices() {
+  try {
+    const { stdout } = await execAsync('adb devices');
+    const lines = stdout.split('\n').slice(1);
+    const devices = [];
+    
+    for (const line of lines) {
+      const parts = line.trim().split(/\s+/);
+      if (parts.length >= 2 && parts[0] && parts[1] === 'device') {
+        devices.push({
+          id: parts[0],
+          status: parts[1]
+        });
+      }
+    }
+    return devices;
+  } catch (error) {
+    console.error('获取设备列表失败:', error);
+    return [];
+  }
+}
+
+/**
+ * IPC 处理程序:扫描 ADB 设备
+ * 
+ * 功能说明:
+ * - 接收渲染进程的扫描请求
+ * - 先执行扫描操作(scanADBDevices)
+ * - 然后获取设备列表(getADBDevices)
+ * - 返回扫描后的设备列表
+ * 
+ * 调用方式(从渲染进程):
+ * ```javascript
+ * const devices = await window.electronAPI.scanADBDevices();
+ * ```
+ * 
+ * @returns {Promise<Array<{id: string, status: string}>>} 设备列表数组
+ */
+ipcMain.handle('scan-adb-devices', async () => {
+  try {
+    await scanADBDevices();
+    return await getADBDevices();
+  } catch (error) {
+    console.error('扫描设备失败:', error);
+    return [];
+  }
+});
+
+/**
+ * IPC 处理程序:获取 ADB 设备列表
+ * 
+ * 功能说明:
+ * - 接收渲染进程的获取设备列表请求
+ * - 直接返回当前已连接的设备列表(不执行扫描)
+ * - 用于刷新设备列表,查看当前连接状态
+ * 
+ * 调用方式(从渲染进程):
+ * ```javascript
+ * const devices = await window.electronAPI.getADBDevices();
+ * ```
+ * 
+ * @returns {Promise<Array<{id: string, status: string}>>} 设备列表数组
+ */
+ipcMain.handle('get-adb-devices', async () => {
+  return await getADBDevices();
+});
+
+/**
+ * IPC 处理程序:连接 ADB 设备
+ * 
+ * 功能说明:
+ * - 接收渲染进程的连接设备请求
+ * - 执行 `adb connect` 命令连接指定设备
+ * - 返回连接结果(成功或失败)
+ * 
+ * 调用方式(从渲染进程):
+ * ```javascript
+ * const result = await window.electronAPI.connectADBDevice('192.168.2.5:5555');
+ * // { success: true } 或 { success: false, error: '错误信息' }
+ * ```
+ * 
+ * @param {Electron.IpcMainInvokeEvent} event - IPC 事件对象
+ * @param {string} ipPort - 设备 IP 地址和端口(格式:'192.168.2.5:5555')
+ * 
+ * @returns {Promise<{success: boolean, error?: string}>} 连接结果
+ * - success: true 表示连接成功,false 表示连接失败
+ * - error: 连接失败时的错误信息(仅在失败时存在)
+ */
+ipcMain.handle('connect-adb-device', async (event, ipPort) => {
+  try {
+    await execAsync(`adb connect ${ipPort}`);
+    return { success: true };
+  } catch (error) {
+    console.error('连接设备失败:', error);
+    return { success: false, error: error.message };
+  }
+});
+
+/**
+ * 应用启动逻辑
+ * 
+ * 当 Electron 应用准备就绪时:
+ * 1. 设置内容安全策略(CSP)
+ * 2. 创建主窗口
+ * 3. 监听 'activate' 事件(macOS 特有,当应用被激活时)
+ *    - 如果没有窗口,则创建新窗口
+ */
 app.whenReady().then(() => {
   setContentSecurityPolicy();
   createWindow();
@@ -50,6 +317,15 @@ app.whenReady().then(() => {
   });
 });
 
+/**
+ * 应用关闭逻辑
+ * 
+ * 当所有窗口都关闭时:
+ * - macOS: 不退出应用(保持应用在 Dock 中运行)
+ * - Windows/Linux: 退出应用
+ * 
+ * 这是 Electron 的标准行为,符合各平台的应用生命周期管理
+ */
 app.on('window-all-closed', () => {
   if (process.platform !== 'darwin') {
     app.quit();

+ 4 - 2
preload.cjs

@@ -1,6 +1,8 @@
-const { contextBridge } = require('electron');
+const { contextBridge, ipcRenderer } = require('electron');
 
 contextBridge.exposeInMainWorld('electronAPI', {
-  // 在这里暴露API给渲染进程
+  scanADBDevices: () => ipcRenderer.invoke('scan-adb-devices'),
+  getADBDevices: () => ipcRenderer.invoke('get-adb-devices'),
+  connectADBDevice: (ipPort) => ipcRenderer.invoke('connect-adb-device', ipPort),
 });
 

+ 1 - 2
src/App.jsx

@@ -1,5 +1,5 @@
 import './App.css';
-import Home from './pages/home.jsx';
+import Home from './pages/Home.jsx';
 
 function App() {
   return (
@@ -8,5 +8,4 @@ function App() {
     </div>
   );
 }
-
 export default App;

+ 26 - 2
src/pages/home.css

@@ -1,3 +1,27 @@
-.home-container {
-  padding: 20px;
+.Home-container {
+  display: flex;
+  width: 100vw;
+  height: 100vh;
+  /* border: 1px solid #f00303; */
+}
+
+.side-bar {
+  width: clamp(250px, 30%, 400px);
+  height: 100%;
+  min-width: 200px;
+}
+
+/* 平板适配 */
+@media (min-width: 768px) and (max-width: 1023px) {
+  .side-bar {
+    width: clamp(200px, 35%, 350px);
+  }
+}
+
+/* 移动端适配 */
+@media (max-width: 767px) {
+  .side-bar {
+    width: 100%;
+    min-width: unset;
+  }
 }

+ 12 - 2
src/pages/home.js

@@ -1,7 +1,17 @@
-export function homeLogic() {
-  // business logic placeholder
+import { useState, useEffect } from 'react';
+
+export function HomeLogic() {
+
+  const [showDevices, setShowDevices] = useState(true);
+
+  useEffect(() => {
+    // console.log('页面渲染完成了');
+    // setShowDevices(true);
+  }, []);
 
   return {
+    showDevices,
+    setShowDevices,
     // expose data or methods here
   };
 }

+ 8 - 5
src/pages/home.jsx

@@ -1,12 +1,15 @@
-import './home.css';
-import { homeLogic } from './home.js';
+import './Home.css';
+import { HomeLogic } from './Home.js';
+import Devices from './Devices/Devices.jsx';
 
 function Home() {
-  const logic = homeLogic();
+  const { showDevices, setShowDevices } = HomeLogic();
 
   return (
-    <div className="home-container">
-      <h1>home</h1>
+    <div className="Home-container">
+      <div className="side-bar">
+        {showDevices && <Devices />}
+      </div>
     </div>
   );
 }