Prechádzať zdrojové kódy

添加json 处理脚本

yichael 3 týždňov pred
rodič
commit
1f756e2f32

+ 1 - 0
.gitignore

@@ -1,5 +1,6 @@
 # Dependencies
 node_modules/
+static/
 package-lock.json
 yarn.lock
 

+ 0 - 124
configs/wait-for-vite.js

@@ -1,124 +0,0 @@
-#!/usr/bin/env node
-/**
- * 等待 Vite 开发服务器启动的脚本
- * 从配置文件读取端口,并等待服务器就绪
- * 如果配置端口被占用,Vite 会自动尝试下一个端口,此脚本会检测实际使用的端口
- */
-
-const path = require('path');
-const http = require('http');
-
-// 获取配置文件
-const configPath = path.join(__dirname, 'config.js');
-const config = require(configPath);
-
-const startPort = config.vite?.port || 5173;
-const viteHost = config.vite?.host || 'localhost';
-const maxWaitTime = 20; // 最多等待 20 秒
-const maxPortAttempts = 10; // 最多尝试 10 个连续端口
-const checkInterval = 500; // 每 500ms 检查一次
-
-/**
- * 检查指定端口是否有 Vite 服务器响应
- * 不仅检查端口是否响应,还要确认是 Vite 服务器(通过检查响应头)
- */
-function checkPort(port) {
-  return new Promise((resolve) => {
-    const req = http.get(`http://${viteHost}:${port}`, (res) => {
-      // 检查状态码
-      if (res.statusCode !== 200) {
-        resolve(false);
-        return;
-      }
-      
-      // 读取响应数据以确认是否是 Vite 服务器
-      let data = '';
-      let resolved = false;
-      
-      res.on('data', (chunk) => {
-        if (resolved) return;
-        data += chunk.toString();
-        // 如果响应包含 Vite 的特征(如 vite/client 或 Vite),确认是 Vite 服务器
-        if (data.length > 100 && (data.includes('/vite') || data.includes('Vite') || data.includes('vite/client'))) {
-          resolved = true;
-          resolve(true);
-        }
-      });
-      
-      res.on('end', () => {
-        if (resolved) return;
-        // 如果响应结束,检查是否包含 Vite 特征
-        // Vite 的 HTML 通常包含 vite/client 或类似的引用
-        if (data.includes('/vite') || data.includes('Vite') || data.includes('vite/client')) {
-          resolve(true);
-        } else {
-          resolve(false);
-        }
-      });
-    });
-    
-    req.on('error', () => {
-      resolve(false);
-    });
-    
-    req.setTimeout(2000, () => {
-      req.destroy();
-      resolve(false);
-    });
-  });
-}
-
-/**
- * 查找 Vite 实际使用的端口
- */
-async function findVitePort() {
-  // 从配置的端口开始,尝试最多 maxPortAttempts 个连续端口
-  for (let offset = 0; offset < maxPortAttempts; offset++) {
-    const port = startPort + offset;
-    const isReady = await checkPort(port);
-    if (isReady) {
-      return port;
-    }
-  }
-  return null;
-}
-
-async function waitForServer() {
-  console.log(`Waiting for Vite dev server (starting from port ${startPort})...`);
-  
-  const startTime = Date.now();
-  let lastFoundPort = null;
-  let consecutiveChecks = 0;
-  
-  while (Date.now() - startTime < maxWaitTime * 1000) {
-    const port = await findVitePort();
-    if (port) {
-      // 如果找到的端口和上次一样,连续检查几次确保稳定
-      if (port === lastFoundPort) {
-        consecutiveChecks++;
-        if (consecutiveChecks >= 3) {
-          // 连续3次检查到同一个端口,确认服务器稳定运行
-          console.log(`\n[OK] Vite dev server is ready at http://${viteHost}:${port}`);
-          process.exit(0);
-        }
-      } else {
-        // 端口变化了,重置计数器
-        lastFoundPort = port;
-        consecutiveChecks = 1;
-      }
-    } else {
-      // 没找到服务器,重置计数器
-      lastFoundPort = null;
-      consecutiveChecks = 0;
-    }
-    
-    // 显示进度
-    process.stdout.write('.');
-    await new Promise(resolve => setTimeout(resolve, checkInterval));
-  }
-  
-  console.log(`\n[ERROR] Vite dev server did not start within ${maxWaitTime} seconds`);
-  process.exit(1);
-}
-
-waitForServer();

+ 8 - 0
doc/CODING_STANDARDS.md

@@ -30,3 +30,11 @@ GUI components must split into three files:
 ## 6. Code Simplicity: Minimal Code
 
 Use the least code to implement functionality. 
+
+## 7. No Console.log
+
+Do not add any `console.log` statements in production code.
+
+## 8. Avoid Creating Script Files
+
+Do not create script files unnecessarily. If you can call PowerShell directly, use PowerShell instead of creating a separate script file.

+ 177 - 0
doc/JSON_PARSER.md

@@ -0,0 +1,177 @@
+# JSON Parser 使用文档
+
+## 功能说明
+
+通过 IPC 调用 `json-parser.js` 进行 JSON 文件的创建、读取、更新和存在性检查。
+
+**重要:** 所有文件只能保存在 `static` 目录下,使用相对路径即可。如果传入 `jason/testjson`,实际文件路径为 `static/jason/testjson.json`。
+
+## 操作类型
+
+### 1. create - 创建 JSON 文件
+
+创建新的 JSON 文件。
+
+**参数:**
+- `operation`: `'create'`
+- `filePath`: JSON 文件相对路径(相对于 `static` 目录),如 `jason/testjson`,会自动添加 `.json` 扩展名
+- `jsonString`: JSON 数据(字符串格式)
+
+**示例:**
+```javascript
+const response = JSON.parse((await window.electronAPI.runNodejsScript('json-parser', 'create', 'jason/testjson', JSON.stringify({devices: [], settings: {}}))).stdout)
+```
+
+**返回:**
+```json
+{
+  "success": true,
+  "message": "JSON file created successfully"
+}
+```
+
+### 2. read - 读取 JSON 文件
+
+读取 JSON 文件内容。
+
+**参数:**
+- `operation`: `'read'`
+- `filePath`: JSON 文件相对路径(相对于 `static` 目录),如 `jason/testjson`
+- `keyPathJson`: (可选)键路径数组,如 `JSON.stringify(['devices', 0, 'ip'])`
+
+**示例:**
+```javascript
+// 读取整个文件
+const data = JSON.parse((await window.electronAPI.runNodejsScript('json-parser', 'read', 'jason/testjson')).stdout).data
+
+// 读取特定路径
+const ip = JSON.parse((await window.electronAPI.runNodejsScript('json-parser', 'read', 'jason/testjson', JSON.stringify(['devices', 0, 'ip']))).stdout).data
+```
+
+**返回:**
+```json
+{
+  "success": true,
+  "data": {...}
+}
+```
+
+### 3. update - 更新 JSON 文件
+
+更新 JSON 文件内容。
+
+**参数:**
+- `operation`: `'update'`
+- `filePath`: JSON 文件相对路径(相对于 `static` 目录),如 `jason/testjson`
+- `jsonString`: 要更新的 JSON 数据(字符串格式)
+- `keyPathJson`: (可选)键路径数组,用于更新特定路径
+
+**示例:**
+```javascript
+// 更新整个文件(合并)
+const response = JSON.parse((await window.electronAPI.runNodejsScript('json-parser', 'update', 'jason/testjson', JSON.stringify({newKey: 'value'}))).stdout)
+
+// 更新特定路径
+const response = JSON.parse((await window.electronAPI.runNodejsScript('json-parser', 'update', 'jason/testjson', JSON.stringify(true), JSON.stringify(['settings', 'autoConnect']))).stdout)
+```
+
+**返回:**
+```json
+{
+  "success": true,
+  "message": "JSON file updated successfully"
+}
+```
+
+### 4. check - 检查文件是否存在
+
+检查 JSON 文件是否存在。
+
+**参数:**
+- `operation`: `'check'`
+- `filePath`: JSON 文件相对路径(相对于 `static` 目录),如 `jason/testjson`
+
+**示例:**
+```javascript
+const exists = JSON.parse((await window.electronAPI.runNodejsScript('json-parser', 'check', 'jason/testjson')).stdout).exists
+```
+
+**返回:**
+```json
+{
+  "success": true,
+  "exists": true
+}
+```
+
+## 封装函数使用(推荐)
+
+在 `device.js` 中已提供封装函数,使用更简洁:
+
+```javascript
+import { 
+  createJsonFile, 
+  readJsonFile, 
+  updateJsonFile, 
+  checkJsonFileExists 
+} from './device.js'
+
+// 创建 JSON 文件
+const result = await createJsonFile('jason/testjson', {devices: []})
+
+// 读取整个文件
+const data = await readJsonFile('jason/testjson')
+
+// 读取特定路径
+const ip = await readJsonFile('jason/testjson', ['devices', 0, 'ip'])
+
+// 更新整个文件
+const result = await updateJsonFile('jason/testjson', {newKey: 'value'})
+
+// 更新特定路径
+const result = await updateJsonFile('jason/testjson', true, ['settings', 'autoConnect'])
+
+// 检查文件是否存在
+const exists = await checkJsonFileExists('jason/testjson')
+```
+
+## 键路径格式
+
+键路径使用数组格式,然后通过 `JSON.stringify()` 序列化:
+
+```javascript
+// 访问 devices[0].ip
+JSON.stringify(['devices', 0, 'ip'])
+
+// 访问 settings.theme
+JSON.stringify(['settings', 'theme'])
+
+// 访问 users[1].profile.name
+JSON.stringify(['users', 1, 'profile', 'name'])
+```
+
+## 注意事项
+
+1. **文件路径**:使用相对路径(相对于 `static` 目录),如 `jason/testjson` 会保存为 `static/jason/testjson.json`
+2. **自动添加扩展名**:如果路径没有 `.json` 扩展名,会自动添加
+3. **路径安全**:禁止使用 `..` 或绝对路径,所有文件只能在 `static` 目录下
+4. **JSON 序列化**:所有 JSON 数据必须使用 `JSON.stringify()` 序列化
+5. **返回值解析**:脚本返回的 `stdout` 是 JSON 字符串,需要 `JSON.parse()` 解析
+6. **错误处理**:检查返回的 `success` 字段判断操作是否成功
+7. **自动创建目录**:如果文件所在目录不存在,会自动创建
+
+## 错误示例
+
+```json
+{
+  "success": false,
+  "error": "JSON file does not exist"
+}
+```
+
+```json
+{
+  "success": false,
+  "error": "Missing jsonString parameter for create operation"
+}
+```

+ 36 - 11
doc/README.md

@@ -32,21 +32,46 @@
 
 **示例:**
 
-```css
-/* 父div宽度 = 50vw,高度 = 30vh */
-/* 宽度比例:50vw * 0.04 = 父div宽度的4% */
-/* 高度比例:30vh * 0.08 = 父div高度的8% */
-font-size: min(calc(50vw * 0.04), calc(30vh * 0.08));
+```scss
+// ========== 字体大小配置 ==========
+$font-size-scale: 1.0;  // 字体缩放系数,调整此值可改变字体大小(数值越大字体越大,越小字体越小)
+
+/* 计算过程:
+ * home-bg grid: Device占第一列20%
+ * device-container: 20vw × 100vh
+ * device-list: 20vw × 80vh (100% × 80% of device-container)
+ * connect-item-container: 80% × 10% of device-list
+ * 宽度:80% × 20vw = 16vw
+ * 高度:10% × 80vh = 8vh
+ * 
+ * 调整字体大小:
+ * - 修改文件顶部的 $font-size-scale 变量
+ * - 增大字体:增加变量值(如改为 1.5 或 2.0)
+ * - 减小字体:减小变量值(如改为 0.5 或 0.8)
+ * - 数值越大,字体越大;数值越小,字体越小
+ */
+font-size: min(calc(16vw * 0.06 * $font-size-scale), calc(8vh * 0.12 * $font-size-scale));
 ```
 
 **参数说明:**
 
-- **`50vw`**:根父容器宽度
-- **`30vh`**:根父容器高度
-- **`0.04`**:字体大小 = 父容器宽度的4%(调整此值可改变字体大小)
-- **`0.08`**:字体大小 = 父容器高度的8%(调整此值可改变字体大小)
+- **`16vw`**:目标元素相对于窗口的宽度(通过层级百分比计算得出)
+- **`8vh`**:目标元素相对于窗口的高度(通过层级百分比计算得出)
+- **`0.06`**:基础宽度比例系数(元素宽度的6%)
+- **`0.12`**:基础高度比例系数(元素高度的12%)
+- **`$font-size-scale`**:字体缩放系数变量,用于统一调整字体大小
 - **`min()`**:取两个计算结果中的较小值,确保字体同时响应宽高变化
 
+**计算方法:**
+1. 从最外层容器开始,逐层计算百分比
+2. 最终得到目标元素相对于窗口的 vw 和 vh 值
+3. 例如:home-bg grid(20%) → device-container(100%) → device-list(100% × 80%) → connect-item-container(80% × 10%)
+4. 结果:16vw × 8vh
+
 **调整方法:**
-- 修改 `0.04` 和 `0.08` 的值来调整字体大小(数值越大字体越大)
-- 修改 `50vw` 和 `30vh` 来调整容器大小
+- **推荐方式**:修改 `$font-size-scale` 变量来调整字体大小
+  - 增大字体:增加变量值(如 `1.5`、`2.0`)
+  - 减小字体:减小变量值(如 `0.5`、`0.8`)
+  - 默认值 `1.0` 保持当前大小
+- **高级调整**:如需单独调整宽度或高度比例,可修改 `0.06` 和 `0.12` 的值
+- 根据实际布局层级重新计算 vw 和 vh 值

+ 2 - 1
enviroment-check.ps1

@@ -47,7 +47,7 @@ if ($npmVersion) {
 Write-Host "`nChecking project dependencies..." -ForegroundColor Yellow
 
 # 调用 nodejs-dependencies-install.js 脚本进行依赖检查和安装
-$nodeDependenciesScript = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "configs\nodejs-dependencies-install.js"
+$nodeDependenciesScript = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "nodejs\dependences\nodejs-dependencies-install.js"
 
 if (Test-Path $nodeDependenciesScript) {
     node $nodeDependenciesScript
@@ -132,3 +132,4 @@ if (Test-Path $pythonDependenciesScript) {
 
 Write-Host "`n================================" -ForegroundColor Cyan
 Write-Host "Environment check completed!" -ForegroundColor Green
+Write-Host "All dependencies are ready. You can now start the project." -ForegroundColor Green

+ 15 - 7
nodejs/adb-connect.js

@@ -2,13 +2,21 @@
 const { execSync } = require('child_process')
 const path = require('path')
 
-// Get adb path relative to project root
 const projectRoot = path.resolve(__dirname, '..')
 const adbPath = path.join(projectRoot, 'exe', 'adb', 'adb.exe')
 
-// Connect to Android device via TCP/IP
-const deviceIp = '192.168.2.5'
-const devicePort = 5555
-const command = `"${adbPath}" connect ${deviceIp}:${devicePort}`
-const output = execSync(command, { encoding: 'utf-8' })
-console.log(output.trim())
+const deviceIp = process.argv[2]
+const devicePort = process.argv[3] || '5555'
+
+if (!deviceIp) {
+  process.stdout.write('false\n')
+  process.exit(1)
+}
+
+const connectCommand = `"${adbPath}" connect ${deviceIp}:${devicePort}`
+const output = execSync(connectCommand, { encoding: 'utf-8' })
+const result = output.trim()
+const isConnected = result.includes('connected') || result.includes('already connected')
+
+process.stdout.write(isConnected ? 'true\n' : 'false\n')
+process.exit(isConnected ? 0 : 1)

+ 0 - 0
configs/dependencies.txt → nodejs/dependences/dependencies.txt


+ 106 - 18
configs/nodejs-dependencies-install.js → nodejs/dependences/nodejs-dependencies-install.js

@@ -10,7 +10,7 @@ const { execSync } = require('child_process');
 
 // 获取脚本所在目录和项目根目录
 const scriptDir = __dirname;
-const projectRoot = path.dirname(scriptDir);
+const projectRoot = path.dirname(path.dirname(scriptDir));
 const packageJsonPath = path.join(projectRoot, 'package.json');
 const dependenciesFile = path.join(scriptDir, 'dependencies.txt');
 const nodeModulesPath = path.join(projectRoot, 'node_modules');
@@ -133,31 +133,119 @@ for (const depName of depNames) {
     }
 }
 
-// 如果有缺失的依赖,显示必要信息并安装
-if (missingCount > 0) {
-    log(`[X] Missing ${missingCount} package(s) out of ${Object.keys(allDependencies).length}`, 'red');
-    
-    log('Missing dependencies:', 'yellow');
-    missingDependencies.forEach(missing => {
-        log(`  - ${missing}`, 'red');
-    });
+// Install missing dependencies with retry loop
+let maxRetries = 5;
+let retryCount = 0;
+let currentMissing = [...missingDependencies];
+
+while (currentMissing.length > 0 && retryCount < maxRetries) {
+    if (retryCount > 0) {
+        log(`\nRetry ${retryCount}/${maxRetries - 1}: Re-checking missing dependencies...`, 'cyan');
+    } else {
+        log(`[X] Missing ${currentMissing.length} package(s) out of ${Object.keys(allDependencies).length}`, 'red');
+        log('Missing dependencies:', 'yellow');
+        currentMissing.forEach(missing => {
+            log(`  - ${missing}`, 'red');
+        });
+    }
     
     log('\nInstalling missing dependencies...', 'yellow');
     
-    try {
-        // 切换到项目根目录
-        process.chdir(projectRoot);
-        // 执行 npm install,隐藏输出
+    // Switch to project root directory
+    process.chdir(projectRoot);
+    
+    // Try multiple npm registries in order
+    const registries = [
+        { name: 'Tencent Cloud', url: 'https://mirrors.cloud.tencent.com/npm/' },
+        { name: 'Huawei Cloud', url: 'https://repo.huaweicloud.com/repository/npm/' },
+        { name: 'Taobao Mirror', url: 'https://registry.npmmirror.com' }
+    ];
+    
+    let installSuccess = false;
+    for (const registry of registries) {
+        log(`\nTrying ${registry.name} registry...`, 'cyan');
+        execSync(`npm config set registry ${registry.url}`, { 
+            stdio: 'inherit',
+            cwd: projectRoot
+        });
+        
+        // Run npm install with output visible
+        const installResult = execSync('npm install', { 
+            stdio: 'inherit',
+            cwd: projectRoot,
+            encoding: 'utf-8'
+        });
+        
+        // Check if installation was successful by re-checking packages
+        const installedPackagesSetAfterInstall = getInstalledPackagesFromFilesystem();
+        const stillMissing = [];
+        for (const depName of depNames) {
+            const depNameLower = depName.toLowerCase();
+            if (!installedPackagesSetAfterInstall.has(depNameLower)) {
+                stillMissing.push(depName);
+            }
+        }
+        
+        if (stillMissing.length === 0) {
+            installSuccess = true;
+            log(`\n[OK] Installation successful using ${registry.name} registry`, 'green');
+            break;
+        } else {
+            log(`\n[~] ${stillMissing.length} package(s) still missing, trying next registry...`, 'yellow');
+        }
+    }
+    
+    if (!installSuccess) {
+        // Final attempt with original registry
+        log('\nTrying default npm registry...', 'cyan');
+        execSync('npm config set registry https://registry.npmjs.org/', { 
+            stdio: 'inherit',
+            cwd: projectRoot
+        });
         execSync('npm install', { 
-            stdio: 'ignore',
+            stdio: 'inherit',
             cwd: projectRoot
         });
+    }
+    
+    // Re-check installed packages after installation
+    log('\nRe-checking installed packages...', 'cyan');
+    const installedPackagesSetAfterInstall = getInstalledPackagesFromFilesystem();
+    const stillMissing = [];
+    
+    for (const depName of depNames) {
+        const depNameLower = depName.toLowerCase();
+        if (!installedPackagesSetAfterInstall.has(depNameLower)) {
+            stillMissing.push(depName);
+        }
+    }
+    
+    if (stillMissing.length === 0) {
         log('[OK] All dependencies installed successfully', 'green');
-    } catch (error) {
-        log('[X] Dependency installation failed', 'red');
-        process.exit(1);
+        currentMissing = [];
+        break;
+    } else if (stillMissing.length < currentMissing.length) {
+        log(`[~] Progress: ${currentMissing.length - stillMissing.length} package(s) installed, ${stillMissing.length} remaining`, 'yellow');
+        currentMissing = stillMissing;
+        retryCount++;
+    } else {
+        log(`[X] Still missing ${stillMissing.length} package(s)`, 'red');
+        stillMissing.forEach(missing => {
+            log(`  - ${missing}`, 'red');
+        });
+        retryCount++;
+        currentMissing = stillMissing;
     }
-} else {
+}
+
+if (currentMissing.length > 0) {
+    log(`\n[X] Failed to install ${currentMissing.length} package(s) after ${retryCount} attempts:`, 'red');
+    currentMissing.forEach(missing => {
+        log(`  - ${missing}`, 'red');
+    });
+    log('\n[WARN] Some packages may require additional setup', 'yellow');
+    process.exit(1);
+} else if (missingCount === 0) {
     log(`[OK] All dependencies are installed (${Object.keys(allDependencies).length} packages)`, 'green');
 }
 

+ 1 - 1
configs/update-nodejs-dependencies.bat → nodejs/dependences/update-nodejs-dependencies.bat

@@ -2,7 +2,7 @@
 chcp 65001 >nul
 title Update Node.js Dependencies List
 
-cd /d "%~dp0\.."
+cd /d "%~dp0\..\.."
 
 REM Check if node_modules exists
 if not exist "node_modules" (

+ 1 - 1
configs/update-nodejs-dependencies.js → nodejs/dependences/update-nodejs-dependencies.js

@@ -9,7 +9,7 @@ const path = require('path');
 
 // 获取脚本所在目录和项目根目录
 const scriptDir = __dirname;
-const projectRoot = path.dirname(scriptDir);
+const projectRoot = path.dirname(path.dirname(scriptDir));
 const dependenciesFile = path.join(scriptDir, 'dependencies.txt');
 const nodeModulesPath = path.join(projectRoot, 'node_modules');
 

+ 113 - 0
nodejs/json-parser.js

@@ -0,0 +1,113 @@
+#!/usr/bin/env node
+const path = require('path')
+const fs = require('fs')
+
+const operation = process.argv[2]
+const relativePath = process.argv[3]
+const jsonString = process.argv[4]
+const keyPathJson = process.argv[5]
+
+if (!operation || !relativePath) {
+  process.stdout.write(JSON.stringify({
+    success: false,
+    error: 'Missing required parameters: operation and filePath'
+  }) + '\n')
+  process.exit(1)
+}
+
+const staticDir = path.resolve(__dirname, '..', 'static')
+const normalizedPath = path.normalize(relativePath).replace(/\\/g, '/')
+if (normalizedPath.includes('..') || normalizedPath.startsWith('/')) {
+  process.stdout.write(JSON.stringify({
+    success: false,
+    error: 'Invalid file path'
+  }) + '\n')
+  process.exit(1)
+}
+
+const fileName = normalizedPath.endsWith('.json') ? normalizedPath : `${normalizedPath}.json`
+const filePath = path.resolve(staticDir, fileName)
+
+if (!filePath.startsWith(staticDir)) {
+  process.stdout.write(JSON.stringify({
+    success: false,
+    error: 'File path must be within static directory'
+  }) + '\n')
+  process.exit(1)
+}
+
+function getNestedValue(obj, keyPath) {
+  let current = obj
+  for (const key of keyPath) {
+    if (current === null || current === undefined) return undefined
+    current = current[key]
+  }
+  return current
+}
+
+function setNestedValue(obj, keyPath, value) {
+  let current = obj
+  for (let i = 0; i < keyPath.length - 1; i++) {
+    const key = keyPath[i]
+    if (current[key] === null || current[key] === undefined) {
+      current[key] = typeof keyPath[i + 1] === 'number' ? [] : {}
+    }
+    current = current[key]
+  }
+  current[keyPath[keyPath.length - 1]] = value
+}
+
+const dir = path.dirname(filePath)
+if (!fs.existsSync(dir)) {
+  fs.mkdirSync(dir, { recursive: true })
+}
+
+let result
+
+if (operation === 'create') {
+  if (!jsonString) {
+    result = { success: false, error: 'Missing jsonString parameter for create operation' }
+  } else {
+    fs.writeFileSync(filePath, JSON.stringify(JSON.parse(jsonString), null, 2), 'utf8')
+    result = { success: true, message: 'JSON file created successfully' }
+  }
+} else if (operation === 'read') {
+  if (!fs.existsSync(filePath)) {
+    result = { success: false, error: 'JSON file does not exist' }
+  } else {
+    const jsonData = JSON.parse(fs.readFileSync(filePath, 'utf8'))
+    result = {
+      success: true,
+      data: keyPathJson ? getNestedValue(jsonData, JSON.parse(keyPathJson)) : jsonData
+    }
+  }
+} else if (operation === 'update') {
+  if (!fs.existsSync(filePath)) {
+    result = { success: false, error: 'JSON file does not exist' }
+  } else if (!jsonString) {
+    result = { success: false, error: 'Missing jsonString parameter for update operation' }
+  } else {
+    const jsonData = JSON.parse(fs.readFileSync(filePath, 'utf8'))
+    const newValue = JSON.parse(jsonString)
+    if (keyPathJson) {
+      setNestedValue(jsonData, JSON.parse(keyPathJson), newValue)
+    } else {
+      Object.assign(jsonData, newValue)
+    }
+    fs.writeFileSync(filePath, JSON.stringify(jsonData, null, 2), 'utf8')
+    result = { success: true, message: 'JSON file updated successfully' }
+  }
+} else if (operation === 'check') {
+  result = {
+    success: true,
+    exists: fs.existsSync(filePath)
+  }
+} else {
+  result = {
+    success: false,
+    error: `Unknown operation: ${operation}. Supported: create, read, update, check`
+  }
+}
+
+process.stdout.write(JSON.stringify(result) + '\n')
+process.exit(result.success ? 0 : 1)

+ 0 - 0
nodejs/scan-connect-ip.js


+ 1 - 1
package.json

@@ -8,7 +8,7 @@
     "build": "vite build",
     "preview": "vite preview",
     "electron": "electron .",
-    "electron:dev": "concurrently \"npm run dev\" \"node configs/wait-for-vite.js && electron .\"",
+    "electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:9527 && electron .\"",
     "electron:build": "npm run build && electron-builder"
   },
   "dependencies": {

+ 25 - 1
run_react.bat

@@ -1,5 +1,29 @@
 @echo off
+chcp 65001 >nul
+title Android Remote Controller - Development
+
+echo Checking environment and installing dependencies...
 powershell -ExecutionPolicy Bypass -File enviroment-check.ps1
+
 if %ERRORLEVEL% EQU 0 (
-    npm run electron:dev
+    echo.
+    echo Starting project...
+    echo Starting Vite dev server...
+    start /B npm run dev
+    echo Waiting for Vite server to be ready...
+    timeout /t 3 /nobreak >nul
+    :wait_loop
+    powershell -Command "try { $response = Invoke-WebRequest -Uri 'http://localhost:9527' -TimeoutSec 1 -UseBasicParsing; exit 0 } catch { exit 1 }" >nul 2>&1
+    if %ERRORLEVEL% NEQ 0 (
+        timeout /t 1 /nobreak >nul
+        goto wait_loop
+    )
+    echo Vite server is ready!
+    echo Starting Electron...
+    npx electron .
+) else (
+    echo.
+    echo Environment check failed. Please fix the errors above.
+    pause
+    exit /b 1
 )

+ 3 - 0
src/page/device/connect-item/connect-item.js

@@ -0,0 +1,3 @@
+// ConnectItem component logic
+// No state or business logic needed for this component
+// ipAddress is passed as prop from parent component

+ 18 - 0
src/page/device/connect-item/connect-item.jsx

@@ -0,0 +1,18 @@
+import React from 'react'
+import './connect-item.scss'
+
+function ConnectItem({ ipAddress ,isConnected=false, isPreviewing=false}) {
+  return (
+    <div className="connect-item-container">
+      <div className="ip-address">{ipAddress}</div>
+      <div className="connect-btn">
+        {isConnected ? '断开' : '连接'}
+      </div>
+      <div className="preview-btn">
+        {isPreviewing ? '停止预览' : '预览'}
+      </div>
+    </div>
+  )
+}
+
+export default ConnectItem

+ 71 - 0
src/page/device/connect-item/connect-item.scss

@@ -0,0 +1,71 @@
+@use '../../public/style/style.scss' as *;
+// ========== 字体大小配置 ==========
+$font-size-scale: 1.5;  // 字体缩放系数,调整此值可改变字体大小(数值越大字体越大,越小字体越小)
+
+@mixin flex-center {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+.connect-item-container {
+  width: 100%;
+  height: 100%;
+
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+
+  margin: 5%;
+  border-radius: 10px;
+  
+    /* 计算过程:
+     * home-bg grid: Device占第一列20%
+     * device-container: 20vw × 100vh
+     * device-list: 20vw × 80vh (100% × 80% of device-container)
+     * connect-item-container: 80% × 10% of device-list
+     * 宽度:80% × 20vw = 16vw
+     * 高度:10% × 80vh = 8vh
+     * 
+     * 调整字体大小:
+     * - 修改文件顶部的 $font-size-scale 变量
+     * - 增大字体:增加变量值(如改为 1.5 或 2.0)
+     * - 减小字体:减小变量值(如改为 0.5 或 0.8)
+     * - 数值越大,字体越大;数值越小,字体越小
+     */
+  font-size: min(calc(16vw * 0.06 * $font-size-scale), calc(8vh * 0.12 * $font-size-scale));
+
+  user-select: none;
+  border: 1px solid #000000;
+}
+
+.ip-address {
+    width: 100%;
+    height: 100%;
+
+    margin: 1%;
+    @include flex-center;
+}
+
+.connect-btn {
+    width: 100%;
+    height: 60%;
+
+    margin: 1%;
+    @include flex-center;
+
+    border-radius: 10px;
+    @include highlight-btn(#004ef6);
+}
+
+.preview-btn {
+    width: 100%;
+    height: 60%;
+
+    margin: 1%;
+    @include flex-center;
+
+    border-radius: 10px;
+    @include highlight-btn(#00fb43);
+}

+ 14 - 1
src/page/device/device.js

@@ -3,6 +3,14 @@ export async function begin(){
     // alert(result.stdout) // 脚本输出
     // const result = await window.electronAPI.runPythonScript('test', '1')
     // alert(result.stdout) // 脚本输出
+
+
+    const is_exist = await window.electronAPI.runNodejsScript('json-parser', 'read', 'jason/testjson')
+    if (is_exist.success) {
+        alert('File exists')
+    } else {
+        alert('File does not exist')
+    }
 }
 
 export async function handleRefresh(e, self) {
@@ -16,6 +24,11 @@ export async function handleRefresh(e, self) {
     }, 5000) 
 }
 
-export function handleAdd() {
+export async function handleAdd() {
     console.log('Add device clicked')
+
+    const ip = document.querySelector('.ip-input input').value
+    const result = await window.electronAPI.runNodejsScript('adb-connect', ip, '5555')
+    alert(result.stdout) // 脚本输出
+
 }

+ 12 - 3
src/page/device/device.jsx

@@ -1,10 +1,12 @@
-import React, { useEffect, useRef } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
 import './device.scss'
-import UpdateBtn from './update-btn.jsx'
+import UpdateBtn from './update-btn/update-btn.jsx'
+import ConnectItem from './connect-item/connect-item.jsx'
 import { handleRefresh, begin, handleAdd } from './device.js'
 
 function Device({ show }) {
   const hasRun = useRef(false)
+  const [deviceList, setDeviceList] = useState([])
 
   if (!show) {
     return null
@@ -20,6 +22,7 @@ function Device({ show }) {
 
   return (
     <div className="device-container">
+      {/* 更新设备列表 */}
       <div className="device-update">
         <div className="device-update-title">设备列表</div>
         <div className="device-update-btn">
@@ -29,9 +32,15 @@ function Device({ show }) {
           />
         </div>
       </div>
-      <div className="device-list">
 
+      {/* 设备列表 */}
+      <div className="device-list">
+        {deviceList.map((device, index) => (
+          <ConnectItem key={index} ipAddress={device.ipAddress} />
+        ))}
       </div>
+
+      {/* 添加设备 */}
       <div className="device-add">
         <div className="ip-input">
           <input type="text" placeholder="请输入设备IP" defaultValue="192.168." />

+ 17 - 30
src/page/device/device.scss

@@ -1,30 +1,4 @@
-// ========== Mixin 配置 ==========
-@mixin flex-center {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-@mixin flex-row-between {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-}
-
-@mixin flex-column-between {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: space-between;
-}
-
-@mixin box-sizing-border-box {
-  box-sizing: border-box;
-  overflow: hidden;
-  margin: 0;
-  padding: 0;
-}
+@use '../public/style/style.scss' as *;
 
 // ========== 样式定义 ==========
 .device-container {
@@ -32,6 +6,7 @@
   height: 100%;
   border: 1px solid #000000;
 
+  // 更新设备列表
   .device-update {
     width: 100%;
     height: 10%;
@@ -51,21 +26,31 @@
       height: 100%;
       @include flex-center;
 
-      .UpdateBtn{
-        transform: scale(0.6);
+      .update-btn {
+        width: 50% !important;
+        height: 50% !important;
       }
     }
 
     background-color: #efe8e88d;
   }
 
+  /* 设备列表 */
   .device-list {
     width: 100%;
     height: 80%;
 
-    // border: 1px solid #000000;
+    @include flex-column-start;
+    border: 1px solid #000000;
+    .connect-item-container {
+      width: 90%;
+      height: 8%;
+      @include flex-row-between;
+      border: 1px solid #000000;
+    }
   }
 
+  // 添加设备
   .device-add {
     width: 100%;
     height: 10%;
@@ -76,7 +61,9 @@
     .ip-input {
       width: 90%;
       height: 50%;
+      
       @include flex-center;
+
       input {
         width: 90%;
         height: 80%;

+ 0 - 0
src/page/device/update-btn.js → src/page/device/update-btn/update-btn.js


+ 11 - 24
src/page/device/update-btn.jsx → src/page/device/update-btn/update-btn.jsx

@@ -1,17 +1,7 @@
 import React, { useRef } from 'react'
-import DivBtn from '../public/div-btn/div-btn.jsx'
 import './update-btn.scss'
 import { useUpdateBtnAnimation } from './update-btn.js'
 
-/**
- * Refresh Button Component
- * @param {Object} props
- * @param {Function} props.onClick - Click handler function (can return Promise)
- * @param {boolean} props.disabled - Disable button (default: false)
- * @param {boolean} props.loading - External loading state (optional)
- * @param {string} props.title - Tooltip text (default: 'Refresh')
- * @param {string} props.className - Additional CSS classes
- */
 function UpdateBtn({
   onClick,
   disabled = false,
@@ -22,7 +12,6 @@ function UpdateBtn({
   const btnRef = useRef(null)
   const { isLoading, handleClick } = useUpdateBtnAnimation(btnRef, externalLoading, onClick, disabled)
   
-  // Default refresh icon
   const refreshIcon = (
     <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
       <path d="M1 4V10H7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
@@ -31,7 +20,6 @@ function UpdateBtn({
     </svg>
   )
 
-  // Loading icon (spinning circle)
   const loadingIcon = (
     <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
       <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" opacity="0.2"/>
@@ -39,7 +27,6 @@ function UpdateBtn({
     </svg>
   )
 
-  // Disabled refresh icon (grayed out)
   const refreshIconDisabled = (
     <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" opacity="0.5">
       <path d="M1 4V10H7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
@@ -49,17 +36,17 @@ function UpdateBtn({
   )
 
   return (
-    <div ref={btnRef}>
-      <DivBtn
-        icon={isLoading ? loadingIcon : refreshIcon}
-        iconDisabled={refreshIconDisabled}
-        onClick={handleClick}
-        disabled={disabled || isLoading}
-        title={isLoading ? 'Loading...' : title}
-        className={`update-btn ${isLoading ? 'update-btn--loading' : ''} ${className}`}
-        rotateOnHover={!isLoading}
-        rotateDegrees={180}
-      />
+    <div 
+      ref={btnRef}
+      className={`update-btn ${isLoading ? 'update-btn--loading' : ''} ${disabled || isLoading ? 'update-btn--disabled' : ''} ${className}`}
+      onClick={handleClick}
+      title={isLoading ? 'Loading...' : title}
+      role="button"
+      tabIndex={disabled || isLoading ? -1 : 0}
+    >
+      <div className="update-btn__icon">
+        {disabled ? refreshIconDisabled : (isLoading ? loadingIcon : refreshIcon)}
+      </div>
     </div>
   )
 }

+ 24 - 23
src/page/device/update-btn.scss → src/page/device/update-btn/update-btn.scss

@@ -5,12 +5,19 @@ $icon-color-pressed: #666;
 
 // ========== 样式定义 ==========
 .update-btn {
-  // Default icon color: black
-  .div-btn__icon {
-    color: $icon-color-black !important;
-    opacity: 1 !important;
-    width: 100% !important;
-    height: 100% !important;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  user-select: none;
+  
+  &__icon {
+    color: $icon-color-black;
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
     
     svg {
       width: 100%;
@@ -18,39 +25,33 @@ $icon-color-pressed: #666;
     }
   }
 
-  // Pressed state: darker color
-  .div-btn--pressed:not(.div-btn--disabled) .div-btn__icon {
-    color: $icon-color-pressed !important;
-    opacity: 1 !important;
+  &:hover:not(&--disabled) {
+    .update-btn__icon {
+      color: $icon-color-pressed;
+    }
   }
 
   &--loading {
     cursor: wait;
     
-    // Hide all icon layers except default
-    .div-btn__icon--hover,
-    .div-btn__icon--pressed,
-    .div-btn__icon--disabled {
-      opacity: 0 !important;
-    }
-    
-    .div-btn__icon--default {
-      opacity: 1 !important;
-      // Loading icon color: gray
-      color: $icon-color-gray !important;
+    .update-btn__icon {
+      color: $icon-color-gray;
       
-      // Rotate loading icon
       svg {
         animation: update-btn-spin 0.5s linear infinite;
         transform-origin: center;
       }
       
-      // Animate loading circle stroke
       svg circle:last-child {
         animation: update-btn-loading 1s linear infinite;
       }
     }
   }
+
+  &--disabled {
+    cursor: not-allowed;
+    opacity: 0.5;
+  }
 }
 
 // ========== 动画定义 ==========

+ 0 - 101
src/page/public/div-btn/div-btn.jsx

@@ -1,101 +0,0 @@
-import React, { useState } from 'react'
-import './div-btn.scss'
-
-/**
- * Icon Button Component
- * @param {Object} props
- * @param {React.ReactNode} props.icon - Default icon (SVG or React component)
- * @param {React.ReactNode} props.iconHover - Hover icon (optional)
- * @param {React.ReactNode} props.iconPressed - Pressed icon (optional)
- * @param {React.ReactNode} props.iconDisabled - Disabled icon (optional)
- * @param {Function} props.onClick - Click handler function
- * @param {string} props.className - Additional CSS classes
- * @param {boolean} props.disabled - Disable button (default: false)
- * @param {string} props.title - Tooltip text
- * @param {boolean} props.rotateOnHover - Rotate icon on hover (default: false)
- * @param {number} props.rotateDegrees - Degrees to rotate on hover (default: 180)
- */
-function DivBtn({
-  icon,
-  iconHover,
-  iconPressed,
-  iconDisabled,
-  onClick,
-  className = '',
-  disabled = false,
-  title = '',
-  rotateOnHover = false,
-  rotateDegrees = 180
-}) {
-  const [isPressed, setIsPressed] = useState(false)
-  const [isHovered, setIsHovered] = useState(false)
-
-  const handleClick = (e) => {
-    if (!disabled && onClick) {
-      onClick(e)
-    }
-  }
-
-  const handleMouseDown = () => {
-    if (!disabled) {
-      setIsPressed(true)
-    }
-  }
-
-  const handleMouseUp = () => {
-    setIsPressed(false)
-  }
-
-  const handleMouseEnter = () => {
-    if (!disabled) {
-      setIsHovered(true)
-    }
-  }
-
-  const handleMouseLeave = () => {
-    setIsHovered(false)
-    setIsPressed(false)
-  }
-
-  return (
-    <div
-      className={`div-btn ${rotateOnHover ? 'div-btn--rotate' : ''} ${disabled ? 'div-btn--disabled' : ''} ${isPressed ? 'div-btn--pressed' : ''} ${className}`}
-      onClick={handleClick}
-      onMouseDown={handleMouseDown}
-      onMouseUp={handleMouseUp}
-      onMouseEnter={handleMouseEnter}
-      onMouseLeave={handleMouseLeave}
-      title={title}
-      role="button"
-      tabIndex={disabled ? -1 : 0}
-      onKeyDown={(e) => {
-        if ((e.key === 'Enter' || e.key === ' ') && !disabled) {
-          e.preventDefault()
-          handleClick(e)
-        }
-      }}
-      style={rotateOnHover ? { '--rotate-degrees': `${rotateDegrees}deg` } : {}}
-    >
-      <div className="div-btn__icon div-btn__icon--default">
-        {icon}
-      </div>
-      {iconHover && (
-        <div className="div-btn__icon div-btn__icon--hover">
-          {iconHover}
-        </div>
-      )}
-      {iconPressed && (
-        <div className="div-btn__icon div-btn__icon--pressed">
-          {iconPressed}
-        </div>
-      )}
-      {iconDisabled && (
-        <div className="div-btn__icon div-btn__icon--disabled">
-          {iconDisabled}
-        </div>
-      )}
-    </div>
-  )
-}
-
-export default DivBtn

+ 0 - 116
src/page/public/div-btn/div-btn.scss

@@ -1,116 +0,0 @@
-// ========== 颜色配置 ==========
-$white: #fff;
-
-// ========== Mixin 配置 ==========
-@mixin flex-center {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-// ========== 样式定义 ==========
-.div-btn {
-  @include flex-center;
-  cursor: pointer;
-  user-select: none;
-  transition: all 0.3s ease;
-  border: none;
-  background: transparent;
-  padding: 0.5em;
-  border-radius: 4px;
-  position: relative;
-
-  // Default size (medium)
-  .div-btn__icon {
-    width: 1.5em;
-    height: 1.5em;
-  }
-
-  &:hover:not(.div-btn--disabled) {
-    background-color: rgba(255, 255, 255, 0.1);
-    transform: scale(1.05);
-  }
-
-  &:active:not(.div-btn--disabled),
-  &--pressed:not(.div-btn--disabled) {
-    transform: scale(0.95);
-  }
-
-  &--disabled {
-    cursor: not-allowed;
-    opacity: 0.5;
-  }
-
-  &--rotate {
-    &:hover:not(.div-btn--disabled) {
-      transform: scale(1.05) rotate(var(--rotate-degrees, 180deg));
-    }
-
-    &:active:not(.div-btn--disabled),
-    &--pressed:not(.div-btn--disabled) {
-      transform: scale(0.95) rotate(360deg);
-    }
-  }
-
-  &__icon {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    color: $white;
-    position: absolute;
-    top: 50%;
-    left: 50%;
-    transform: translate(-50%, -50%);
-    transition: opacity 0.2s ease;
-
-    &--default {
-      opacity: 1;
-    }
-
-    &--hover {
-      opacity: 0;
-    }
-
-    &--pressed {
-      opacity: 0;
-    }
-
-    &--disabled {
-      opacity: 0;
-    }
-  }
-
-  // Show hover icon on hover
-  &:hover:not(.div-btn--disabled):not(.div-btn--pressed) {
-    .div-btn__icon--default {
-      opacity: 0;
-    }
-    .div-btn__icon--hover {
-      opacity: 1;
-    }
-  }
-
-  // Show pressed icon when pressed
-  &:active:not(.div-btn--disabled),
-  &--pressed:not(.div-btn--disabled) {
-    .div-btn__icon--default,
-    .div-btn__icon--hover {
-      opacity: 0;
-    }
-    .div-btn__icon--pressed {
-      opacity: 1;
-    }
-  }
-
-  // Show disabled icon when disabled
-  &--disabled {
-    .div-btn__icon--default,
-    .div-btn__icon--hover,
-    .div-btn__icon--pressed {
-      opacity: 0;
-    }
-    .div-btn__icon--disabled {
-      opacity: 1;
-    }
-  }
-}

+ 82 - 0
src/page/public/style/style.scss

@@ -0,0 +1,82 @@
+@use "sass:color";
+
+// 按钮高亮
+@mixin highlight-btn($base-color) {
+    $light-color: color.adjust($base-color, $lightness: 8%);
+    $dark-color: color.adjust($base-color, $lightness: -10%);
+    $hover-light: color.adjust($base-color, $lightness: 12%);
+    $hover-dark: color.adjust($base-color, $lightness: 2%);
+    $active-dark: color.adjust($base-color, $lightness: -15%);
+    $active-darker: color.adjust($base-color, $lightness: -20%);
+    
+    background: linear-gradient(135deg, $light-color 0%, $dark-color 100%);
+    box-shadow: 
+        inset 0 1px 2px rgba(255, 255, 255, 0.3),
+        inset 0 -1px 2px rgba(0, 0, 0, 0.3),
+        0 2px 4px rgba(0, 0, 0, 0.2);
+    position: relative;
+    overflow: hidden;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    
+    &::before {
+        content: '';
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        height: 50%;
+        background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, transparent 100%);
+        pointer-events: none;
+    }
+    
+    &:hover {
+        background: linear-gradient(135deg, $hover-light 0%, $hover-dark 100%);
+        box-shadow: 
+            inset 0 1px 2px rgba(255, 255, 255, 0.4),
+            inset 0 -1px 2px rgba(0, 0, 0, 0.3),
+            0 3px 6px rgba(0, 0, 0, 0.3);
+    }
+    
+    &:active {
+        background: linear-gradient(135deg, $active-dark 0%, $active-darker 100%);
+        box-shadow: 
+            inset 0 2px 4px rgba(0, 0, 0, 0.4),
+            0 1px 2px rgba(0, 0, 0, 0.2);
+    }
+}
+
+// ========== Mixin 配置 ==========
+@mixin flex-center {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  
+  @mixin flex-row-between {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+  }
+  
+  @mixin flex-column-start {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: flex-start;
+  }
+  
+  @mixin flex-column-between {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: space-between;
+  }
+  
+  @mixin box-sizing-border-box {
+    box-sizing: border-box;
+    overflow: hidden;
+    margin: 0;
+    padding: 0;
+  }