Browse Source

nodejs 环境分类检测。画布无线移动

yichael 1 week ago
parent
commit
4922f8c40f

+ 2 - 0
doc/代码规范.md

@@ -53,3 +53,5 @@ if (condition) {
 ## 13. 不进行存在性检查
 ## 13. 不进行存在性检查
 
 
 不要添加 if 语句来检查目录是否存在、文件是否存在或解析是否成功。让错误崩溃。
 不要添加 if 语句来检查目录是否存在、文件是否存在或解析是否成功。让错误崩溃。
+
+## 14. 所有传进来的参数都不要验证,要假设所有参数已经经过验证没有问题。

+ 10 - 3
enviroment-check.ps1

@@ -48,11 +48,14 @@ if ($npmVersion) {
     npm --version
     npm --version
 }
 }
 
 
+# 根据当前架构确定 x64 或 arm64
+$arch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "x64" }
+
 # Check if dependencies are installed
 # Check if dependencies are installed
 Write-Host "`nChecking project dependencies..." -ForegroundColor Yellow
 Write-Host "`nChecking project dependencies..." -ForegroundColor Yellow
 
 
-# 调用 nodejs-dependencies-install.js 脚本进行依赖检查和安装
-$nodeDependenciesScript = Join-Path $scriptRoot "nodejs\dependences\nodejs-dependencies-install.js"
+# 调用对应架构的 nodejs-dependencies-install.js 进行依赖检查和安装
+$nodeDependenciesScript = Join-Path $scriptRoot "nodejs\dependences\$arch\nodejs-dependencies-install.js"
 
 
 if (Test-Path $nodeDependenciesScript) {
 if (Test-Path $nodeDependenciesScript) {
     node $nodeDependenciesScript
     node $nodeDependenciesScript
@@ -60,6 +63,11 @@ if (Test-Path $nodeDependenciesScript) {
         Write-Host "[X] Node dependencies check/installation failed" -ForegroundColor Red
         Write-Host "[X] Node dependencies check/installation failed" -ForegroundColor Red
         exit 1
         exit 1
     }
     }
+    # 生成 x64 和 arm64 两份 dependencies.txt
+    $generateScript = Join-Path $scriptRoot "nodejs\dependences\generate-nodejs-dependencies.js"
+    if (Test-Path $generateScript) {
+        node $generateScript
+    }
 } else {
 } else {
     Write-Host "[X] nodejs-dependencies-install.js not found at: $nodeDependenciesScript" -ForegroundColor Red
     Write-Host "[X] nodejs-dependencies-install.js not found at: $nodeDependenciesScript" -ForegroundColor Red
     Write-Host "[WARN] Continuing without dependency check..." -ForegroundColor Yellow
     Write-Host "[WARN] Continuing without dependency check..." -ForegroundColor Yellow
@@ -103,7 +111,6 @@ if ($pipVersion) {
 
 
 #check python virtual environment(venv 在 python/x64/env 或 python/arm64/env)
 #check python virtual environment(venv 在 python/x64/env 或 python/arm64/env)
 Write-Host "`nChecking python virtual environment..." -ForegroundColor Yellow
 Write-Host "`nChecking python virtual environment..." -ForegroundColor Yellow
-$arch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "x64" }
 $_p = node (Join-Path $scriptRoot "configs\get-python-env-path.js") 2>$null
 $_p = node (Join-Path $scriptRoot "configs\get-python-env-path.js") 2>$null
 $venvPath = if ($_p -and $_p -ne "undefined") { $_p.Trim() } else { Join-Path $scriptRoot "python\$arch\env" }
 $venvPath = if ($_p -and $_p -ne "undefined") { $_p.Trim() } else { Join-Path $scriptRoot "python\$arch\env" }
 if (Test-Path $venvPath) {
 if (Test-Path $venvPath) {

+ 327 - 0
nodejs/dependences/arm64/nodejs-dependencies-install.js

@@ -0,0 +1,327 @@
+#!/usr/bin/env node
+/**
+ * Node.js 依赖安装和同步脚本
+ * 功能:检查、安装 package.json 中的依赖,然后同步所有已安装的包到 dependencies.txt
+ */
+
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+// 获取脚本所在目录和项目根目录(scriptDir = nodejs/dependences/arm64,项目根 = 再上一级)
+const scriptDir = __dirname;
+const projectRoot = path.dirname(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');
+
+// 颜色输出函数
+const colors = {
+    reset: '\x1b[0m',
+    red: '\x1b[31m',
+    green: '\x1b[32m',
+    yellow: '\x1b[33m',
+    cyan: '\x1b[36m',
+    white: '\x1b[37m'
+};
+
+function log(message, color = 'reset') {
+    console.log(`${colors[color]}${message}${colors.reset}`);
+}
+
+// 检查 package.json 是否存在
+if (!fs.existsSync(packageJsonPath)) {
+    log('[X] package.json not found', 'red');
+    process.exit(1);
+}
+
+// 读取 package.json
+const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
+const allDependencies = {};
+
+// 收集所有依赖
+if (packageJson.dependencies) {
+    Object.keys(packageJson.dependencies).forEach(depName => {
+        allDependencies[depName] = {
+            version: packageJson.dependencies[depName],
+            type: 'dependency'
+        };
+    });
+}
+
+if (packageJson.devDependencies) {
+    Object.keys(packageJson.devDependencies).forEach(depName => {
+        allDependencies[depName] = {
+            version: packageJson.devDependencies[depName],
+            type: 'devDependency'
+        };
+    });
+}
+
+// 快速获取已安装的包列表(直接从 node_modules 文件夹读取)
+function getInstalledPackagesFromFilesystem() {
+    const installedPackages = new Set();
+    
+    if (!fs.existsSync(nodeModulesPath)) {
+        return installedPackages;
+    }
+    
+    try {
+        const entries = fs.readdirSync(nodeModulesPath, { withFileTypes: true });
+        
+        for (const entry of entries) {
+            if (entry.isDirectory()) {
+                const packageName = entry.name;
+                
+                // 跳过特殊目录
+                if (packageName.startsWith('.') || packageName === 'node_modules') {
+                    continue;
+                }
+                
+                // 检查是否是有效的包(有 package.json)
+                const packageJsonPath = path.join(nodeModulesPath, packageName, 'package.json');
+                if (fs.existsSync(packageJsonPath)) {
+                    installedPackages.add(packageName.toLowerCase());
+                }
+                
+                // 处理 scoped 包(如 @babel/core)
+                if (packageName.startsWith('@')) {
+                    try {
+                        const scopedPath = path.join(nodeModulesPath, packageName);
+                        const scopedEntries = fs.readdirSync(scopedPath, { withFileTypes: true });
+                        for (const scopedEntry of scopedEntries) {
+                            if (scopedEntry.isDirectory()) {
+                                const scopedPackageName = `${packageName}/${scopedEntry.name}`;
+                                const scopedPackageJsonPath = path.join(scopedPath, scopedEntry.name, 'package.json');
+                                if (fs.existsSync(scopedPackageJsonPath)) {
+                                    installedPackages.add(scopedPackageName.toLowerCase());
+                                }
+                            }
+                        }
+                    } catch (error) {
+                        // 忽略错误
+                    }
+                }
+            }
+        }
+    } catch (error) {
+        // 忽略错误
+    }
+    
+    return installedPackages;
+}
+
+// 快速检查缺失的依赖(使用文件系统)
+const missingDependencies = [];
+let installedCount = 0;
+let missingCount = 0;
+
+// 一次性获取所有已安装的包(只检查一次文件系统)
+const installedPackagesSet = getInstalledPackagesFromFilesystem();
+
+const depNames = Object.keys(allDependencies).sort();
+
+for (const depName of depNames) {
+    const depNameLower = depName.toLowerCase();
+    
+    // 快速检查(使用已获取的集合)
+    if (installedPackagesSet.has(depNameLower)) {
+        installedCount++;
+    } else {
+        missingDependencies.push(depName);
+        missingCount++;
+    }
+}
+
+// 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');
+    
+    // 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: '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');
+        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;
+    }
+}
+
+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');
+}
+
+// 快速同步所有已安装的依赖到 dependencies.txt(只读取根级包,静默执行)
+function syncInstalledPackagesToFile() {
+    const syncResult = [];
+    
+    if (!fs.existsSync(nodeModulesPath)) {
+        return syncResult;
+    }
+    
+    try {
+        const entries = fs.readdirSync(nodeModulesPath, { withFileTypes: true });
+        
+        for (const entry of entries) {
+            if (entry.isDirectory()) {
+                const packageName = entry.name;
+                
+                // 跳过特殊目录
+                if (packageName.startsWith('.') || packageName === 'node_modules') {
+                    continue;
+                }
+                
+                // 处理普通包
+                const packageJsonPath = path.join(nodeModulesPath, packageName, 'package.json');
+                if (fs.existsSync(packageJsonPath)) {
+                    try {
+                        const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
+                        if (pkg.name && pkg.version) {
+                            syncResult.push(`${pkg.name}==${pkg.version}`);
+                        }
+                    } catch (error) {
+                        // 忽略解析错误
+                    }
+                }
+                
+                // 处理 scoped 包(如 @babel/core)
+                if (packageName.startsWith('@')) {
+                    try {
+                        const scopedPath = path.join(nodeModulesPath, packageName);
+                        const scopedEntries = fs.readdirSync(scopedPath, { withFileTypes: true });
+                        for (const scopedEntry of scopedEntries) {
+                            if (scopedEntry.isDirectory()) {
+                                const scopedPackageJsonPath = path.join(scopedPath, scopedEntry.name, 'package.json');
+                                if (fs.existsSync(scopedPackageJsonPath)) {
+                                    try {
+                                        const pkg = JSON.parse(fs.readFileSync(scopedPackageJsonPath, 'utf-8'));
+                                        if (pkg.name && pkg.version) {
+                                            syncResult.push(`${pkg.name}==${pkg.version}`);
+                                        }
+                                    } catch (error) {
+                                        // 忽略解析错误
+                                    }
+                                }
+                            }
+                        }
+                    } catch (error) {
+                        // 忽略错误
+                    }
+                }
+            }
+        }
+    } catch (error) {
+        // 忽略错误
+    }
+    
+    return syncResult;
+}
+
+// 同步所有已安装的依赖到 dependencies.txt(快速方法,静默执行)
+const syncResult = syncInstalledPackagesToFile();
+
+// 去重并排序
+const uniqueResult = [...new Set(syncResult)].sort();
+
+// 写入文件(UTF-8 编码)
+fs.writeFileSync(dependenciesFile, uniqueResult.join('\n') + '\n', 'utf-8');
+
+process.exit(0);

+ 0 - 0
nodejs/dependences/update-nodejs-dependencies.bat → nodejs/dependences/arm64/update-nodejs-dependencies.bat


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

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

+ 0 - 325
nodejs/dependences/dependencies.txt

@@ -1,325 +0,0 @@
-7zip-bin==5.2.0
-@babel/code-frame==7.29.0
-@babel/compat-data==7.29.0
-@babel/core==7.29.0
-@babel/generator==7.29.1
-@babel/helper-compilation-targets==7.28.6
-@babel/helper-globals==7.28.0
-@babel/helper-module-imports==7.28.6
-@babel/helper-module-transforms==7.28.6
-@babel/helper-plugin-utils==7.28.6
-@babel/helper-string-parser==7.27.1
-@babel/helper-validator-identifier==7.28.5
-@babel/helper-validator-option==7.27.1
-@babel/helpers==7.28.6
-@babel/parser==7.29.0
-@babel/plugin-transform-react-jsx-self==7.27.1
-@babel/plugin-transform-react-jsx-source==7.27.1
-@babel/template==7.28.6
-@babel/traverse==7.29.0
-@babel/types==7.29.0
-@develar/schema-utils==2.6.5
-@electron/asar==3.4.1
-@electron/get==2.0.3
-@electron/notarize==2.2.1
-@electron/osx-sign==1.0.5
-@electron/universal==1.5.1
-@esbuild/win32-arm64==0.27.3
-@hapi/hoek==9.3.0
-@hapi/topo==5.1.0
-@isaacs/cliui==8.0.2
-@jridgewell/gen-mapping==0.3.13
-@jridgewell/remapping==2.3.5
-@jridgewell/resolve-uri==3.1.2
-@jridgewell/sourcemap-codec==1.5.5
-@jridgewell/trace-mapping==0.3.31
-@malept/cross-spawn-promise==1.1.1
-@malept/flatpak-bundler==0.4.0
-@parcel/watcher-win32-arm64==2.5.6
-@parcel/watcher==2.5.6
-@pkgjs/parseargs==0.11.0
-@remix-run/router==1.23.2
-@rolldown/pluginutils==1.0.0-rc.2
-@rollup/rollup-win32-arm64-msvc==4.57.1
-@sideway/address==4.1.5
-@sideway/formula==3.0.1
-@sideway/pinpoint==2.0.0
-@sindresorhus/is==4.6.0
-@szmarczak/http-timer==4.0.6
-@tootallnate/once==2.0.0
-@types/babel__core==7.20.5
-@types/babel__generator==7.27.0
-@types/babel__template==7.4.4
-@types/babel__traverse==7.28.0
-@types/cacheable-request==6.0.3
-@types/debug==4.1.12
-@types/estree==1.0.8
-@types/fs-extra==9.0.13
-@types/http-cache-semantics==4.2.0
-@types/keyv==3.1.4
-@types/ms==2.1.0
-@types/node==25.2.2
-@types/responselike==1.0.3
-@types/yauzl==2.10.3
-@vitejs/plugin-react==5.1.3
-@xmldom/xmldom==0.8.11
-agent-base==6.0.2
-ajv-keywords==3.5.2
-ajv==6.12.6
-ansi-regex==5.0.1
-ansi-styles==4.3.0
-app-builder-bin==4.0.0
-app-builder-lib==24.13.3
-archiver-utils==2.1.0
-archiver==5.3.2
-argparse==2.0.1
-async-exit-hook==2.0.1
-async==3.2.6
-asynckit==0.4.0
-at-least-node==1.0.0
-axios==1.13.5
-balanced-match==1.0.2
-base64-js==1.5.1
-baseline-browser-mapping==2.9.19
-bl==4.1.0
-bluebird-lst==1.0.9
-bluebird==3.7.2
-boolean==3.2.0
-brace-expansion==2.0.2
-browserslist==4.28.1
-buffer-crc32==0.2.13
-buffer-equal==1.0.1
-buffer-from==1.1.2
-buffer==5.7.1
-builder-util-runtime==9.2.4
-builder-util==24.13.1
-cacheable-lookup==5.0.4
-cacheable-request==7.0.4
-call-bind-apply-helpers==1.0.2
-caniuse-lite==1.0.30001769
-chalk==4.1.2
-chokidar==4.0.3
-chownr==2.0.0
-chromium-pickle-js==0.2.0
-ci-info==3.9.0
-cliui==8.0.1
-clone-response==1.0.3
-color-convert==2.0.1
-color-name==1.1.4
-combined-stream==1.0.8
-commander==5.1.0
-compare-version==0.1.2
-compress-commons==4.1.2
-concat-map==0.0.1
-concurrently==9.2.1
-config-file-ts==0.2.6
-convert-source-map==2.0.0
-core-util-is==1.0.2
-crc-32==1.2.2
-crc32-stream==4.0.3
-cross-spawn==7.0.6
-debug==4.4.3
-decompress-response==6.0.0
-defer-to-connect==2.0.1
-define-data-property==1.1.4
-define-properties==1.2.1
-delayed-stream==1.0.0
-detect-libc==2.1.2
-detect-node==2.1.0
-dir-compare==3.3.0
-dmg-builder==24.13.3
-dotenv-expand==5.1.0
-dotenv==9.0.2
-dunder-proto==1.0.1
-eastasianwidth==0.2.0
-ejs==3.1.10
-electron-builder-squirrel-windows==24.13.3
-electron-builder==24.13.3
-electron-publish==24.13.1
-electron-to-chromium==1.5.286
-electron==28.3.3
-emoji-regex==8.0.0
-end-of-stream==1.4.5
-env-paths==2.2.1
-err-code==2.0.3
-es-define-property==1.0.1
-es-errors==1.3.0
-es-object-atoms==1.1.1
-es-set-tostringtag==2.1.0
-es6-error==4.1.1
-esbuild==0.27.3
-escalade==3.2.0
-escape-string-regexp==4.0.0
-extract-zip==2.0.1
-fast-deep-equal==3.1.3
-fast-json-stable-stringify==2.1.0
-fd-slicer==1.1.0
-fdir==6.5.0
-filelist==1.0.4
-follow-redirects==1.15.11
-foreground-child==3.3.1
-form-data==4.0.5
-fs-constants==1.0.0
-fs-extra==8.1.0
-fs-minipass==2.1.0
-fs.realpath==1.0.0
-function-bind==1.1.2
-gensync==1.0.0-beta.2
-get-caller-file==2.0.5
-get-intrinsic==1.3.0
-get-proto==1.0.1
-get-stream==5.2.0
-glob==7.2.3
-global-agent==3.0.0
-globalthis==1.0.4
-gopd==1.2.0
-got==11.8.6
-graceful-fs==4.2.11
-has-flag==4.0.0
-has-property-descriptors==1.0.2
-has-symbols==1.1.0
-has-tostringtag==1.0.2
-hasown==2.0.2
-hosted-git-info==4.1.0
-http-cache-semantics==4.2.0
-http-proxy-agent==5.0.0
-http2-wrapper==1.0.3
-https-proxy-agent==5.0.1
-iconv-lite==0.6.3
-ieee754==1.2.1
-immutable==5.1.4
-inflight==1.0.6
-inherits==2.0.4
-is-ci==3.0.1
-is-extglob==2.1.1
-is-fullwidth-code-point==3.0.0
-is-glob==4.0.3
-isarray==1.0.0
-isbinaryfile==5.0.7
-isexe==2.0.0
-jackspeak==3.4.3
-jake==10.9.4
-joi==17.13.3
-js-tokens==4.0.0
-js-yaml==4.1.1
-jsesc==3.1.0
-json-buffer==3.0.1
-json-schema-traverse==0.4.1
-json-stringify-safe==5.0.1
-json5==2.2.3
-jsonfile==4.0.0
-keyv==4.5.4
-lazy-val==1.0.5
-lazystream==1.0.1
-lodash.defaults==4.2.0
-lodash.difference==4.5.0
-lodash.flatten==4.4.0
-lodash.isplainobject==4.0.6
-lodash.union==4.6.0
-lodash==4.17.23
-loose-envify==1.4.0
-lowercase-keys==2.0.0
-lru-cache==5.1.1
-matcher==3.0.0
-math-intrinsics==1.1.0
-mime-db==1.52.0
-mime-types==2.1.35
-mime==2.6.0
-mimic-response==1.0.1
-minimatch==5.1.6
-minimist==1.2.8
-minipass==5.0.0
-minizlib==2.1.2
-mkdirp==1.0.4
-ms==2.1.3
-nanoid==3.3.11
-node-releases==2.0.27
-normalize-path==3.0.0
-normalize-url==6.1.0
-object-keys==1.1.1
-once==1.4.0
-p-cancelable==2.1.1
-package-json-from-dist==1.0.1
-path-is-absolute==1.0.1
-path-key==3.1.1
-path-scurry==1.11.1
-pend==1.2.0
-picocolors==1.1.1
-picomatch==4.0.3
-plist==3.1.0
-process-nextick-args==2.0.1
-progress==2.0.3
-promise-retry==2.0.1
-proxy-from-env==1.1.0
-pump==3.0.3
-punycode==2.3.1
-quick-lru==5.1.1
-react-dom==18.3.1
-react-refresh==0.18.0
-react-router-dom==6.30.3
-react-router==6.30.3
-react==18.3.1
-read-config-file==6.3.2
-readable-stream==3.6.2
-readdir-glob==1.1.3
-readdirp==4.1.2
-require-directory==2.1.1
-resolve-alpn==1.2.1
-responselike==2.0.1
-retry==0.12.0
-roarr==2.15.4
-rollup==4.57.1
-rxjs==7.8.2
-safe-buffer==5.2.1
-safer-buffer==2.1.2
-sanitize-filename==1.6.3
-sass==1.97.3
-sax==1.4.4
-scheduler==0.23.2
-semver-compare==1.0.0
-semver==6.3.1
-serialize-error==7.0.1
-shebang-command==2.0.0
-shebang-regex==3.0.0
-shell-quote==1.8.3
-signal-exit==4.1.0
-simple-update-notifier==2.0.0
-source-map-js==1.2.1
-source-map-support==0.5.21
-source-map==0.6.1
-sprintf-js==1.1.3
-stat-mode==1.0.0
-string-width==4.2.3
-string_decoder==1.3.0
-strip-ansi==6.0.1
-sumchecker==3.0.1
-supports-color==8.1.1
-tar-stream==2.2.0
-tar==6.2.1
-temp-file==3.4.0
-tinyglobby==0.2.15
-tmp-promise==3.0.3
-tmp==0.2.5
-tree-kill==1.2.2
-truncate-utf8-bytes==1.0.2
-tslib==2.8.1
-type-fest==0.13.1
-typescript==5.9.3
-undici-types==7.16.0
-universalify==0.1.2
-update-browserslist-db==1.2.3
-uri-js==4.4.1
-utf8-byte-length==1.0.5
-util-deprecate==1.0.2
-vite==7.3.1
-wait-on==7.2.0
-which==2.0.2
-wrap-ansi==7.0.0
-wrappy==1.0.2
-xmlbuilder==15.1.1
-y18n==5.0.8
-yallist==3.1.1
-yargs-parser==21.1.1
-yargs==17.7.2
-yauzl==2.10.0
-zip-stream==4.1.1

+ 122 - 0
nodejs/dependences/generate-nodejs-dependencies.js

@@ -0,0 +1,122 @@
+#!/usr/bin/env node
+/**
+ * 根据 x64 和 arm64 架构分别生成 dependencies.txt
+ * 功能:从 node_modules 读取已安装包,按架构替换平台相关包,分别写入 x64 和 arm64 目录
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+const scriptDir = __dirname;
+const projectRoot = path.dirname(path.dirname(scriptDir));
+const nodeModulesPath = path.join(projectRoot, 'node_modules');
+const x64Dir = path.join(scriptDir, 'x64');
+const arm64Dir = path.join(scriptDir, 'arm64');
+
+// 平台相关包的映射(x64 ↔ arm64 一一对应)
+const X64_TO_ARM64 = {
+    '@esbuild/win32-x64': '@esbuild/win32-arm64',
+    '@parcel/watcher-win32-x64': '@parcel/watcher-win32-arm64',
+    '@rollup/rollup-win32-x64-gnu': '@rollup/rollup-win32-arm64-gnu',
+    '@rollup/rollup-win32-x64-msvc': '@rollup/rollup-win32-arm64-msvc'
+};
+const ARM64_TO_X64 = Object.fromEntries(
+    Object.entries(X64_TO_ARM64).map(([k, v]) => [v, k])
+);
+
+const colors = {
+    reset: '\x1b[0m',
+    green: '\x1b[32m',
+    yellow: '\x1b[33m',
+    cyan: '\x1b[36m'
+};
+
+function log(message, color = 'reset') {
+    console.log(`${colors[color]}${message}${colors.reset}`);
+}
+
+function collectPackagesFromNodeModules() {
+    const packages = new Map();
+
+    if (!fs.existsSync(nodeModulesPath)) {
+        return packages;
+    }
+
+    function scan(dir, prefix = '') {
+        const entries = fs.readdirSync(dir, { withFileTypes: true });
+        for (const entry of entries) {
+            if (entry.isDirectory() && !entry.name.startsWith('.')) {
+                const fullName = prefix ? `${prefix}/${entry.name}` : entry.name;
+                const pkgPath = path.join(dir, entry.name, 'package.json');
+                if (fs.existsSync(pkgPath)) {
+                    try {
+                        const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
+                        if (pkg.name && pkg.version) {
+                            packages.set(pkg.name.toLowerCase(), `${pkg.name}==${pkg.version}`);
+                        }
+                    } catch (_) {}
+                }
+                if (entry.name.startsWith('@')) {
+                    scan(path.join(dir, entry.name), entry.name);
+                }
+            }
+        }
+    }
+
+    scan(nodeModulesPath);
+    return packages;
+}
+
+function buildArchList(packagesMap, targetArch) {
+    const toExclude = targetArch === 'x64' ? Object.values(X64_TO_ARM64) : Object.keys(X64_TO_ARM64);
+    const swapMap = targetArch === 'x64' ? ARM64_TO_X64 : X64_TO_ARM64;
+
+    const result = [];
+    for (const [nameLower, nameVersion] of packagesMap) {
+        if (toExclude.some(p => nameLower === p.toLowerCase())) {
+            continue;
+        }
+        result.push(nameVersion);
+    }
+
+    // 若 node_modules 中有对方架构的平台包,为目标架构添加对应的同版本包
+    for (const [fromPkg, toPkg] of Object.entries(swapMap)) {
+        const fromEntry = [...packagesMap.entries()].find(([k]) => k === fromPkg.toLowerCase());
+        if (fromEntry) {
+            const ver = fromEntry[1].includes('==') ? fromEntry[1].split('==')[1] : 'latest';
+            const line = `${toPkg}==${ver}`;
+            if (!result.some(r => r.startsWith(toPkg + '=='))) {
+                result.push(line);
+            }
+        }
+    }
+
+    return [...new Set(result)].sort();
+}
+
+function main() {
+    log('Generating x64 and arm64 dependencies.txt...', 'cyan');
+
+    const packagesMap = collectPackagesFromNodeModules();
+    if (packagesMap.size === 0) {
+        log('[WARN] node_modules not found or empty. Run npm install first.', 'yellow');
+        process.exit(1);
+    }
+
+    const x64List = buildArchList(packagesMap, 'x64');
+    const arm64List = buildArchList(packagesMap, 'arm64');
+
+    if (!fs.existsSync(x64Dir)) fs.mkdirSync(x64Dir, { recursive: true });
+    if (!fs.existsSync(arm64Dir)) fs.mkdirSync(arm64Dir, { recursive: true });
+
+    const x64File = path.join(x64Dir, 'dependencies.txt');
+    const arm64File = path.join(arm64Dir, 'dependencies.txt');
+
+    fs.writeFileSync(x64File, x64List.join('\n') + '\n', 'utf-8');
+    fs.writeFileSync(arm64File, arm64List.join('\n') + '\n', 'utf-8');
+
+    log(`[OK] x64/dependencies.txt: ${x64List.length} packages`, 'green');
+    log(`[OK] arm64/dependencies.txt: ${arm64List.length} packages`, 'green');
+}
+
+main();

+ 2 - 2
nodejs/dependences/nodejs-dependencies-install.js → nodejs/dependences/x64/nodejs-dependencies-install.js

@@ -8,9 +8,9 @@ const fs = require('fs');
 const path = require('path');
 const path = require('path');
 const { execSync } = require('child_process');
 const { execSync } = require('child_process');
 
 
-// 获取脚本所在目录和项目根目录
+// 获取脚本所在目录和项目根目录(scriptDir = nodejs/dependences/x64,项目根 = 再上一级)
 const scriptDir = __dirname;
 const scriptDir = __dirname;
-const projectRoot = path.dirname(path.dirname(scriptDir));
+const projectRoot = path.dirname(path.dirname(path.dirname(scriptDir)));
 const packageJsonPath = path.join(projectRoot, 'package.json');
 const packageJsonPath = path.join(projectRoot, 'package.json');
 const dependenciesFile = path.join(scriptDir, 'dependencies.txt');
 const dependenciesFile = path.join(scriptDir, 'dependencies.txt');
 const nodeModulesPath = path.join(projectRoot, 'node_modules');
 const nodeModulesPath = path.join(projectRoot, 'node_modules');

+ 30 - 0
nodejs/dependences/x64/update-nodejs-dependencies.bat

@@ -0,0 +1,30 @@
+@echo off
+chcp 65001 >nul
+title Update Node.js Dependencies List
+
+cd /d "%~dp0\..\.."
+
+REM Check if node_modules exists
+if not exist "node_modules" (
+    echo [ERROR] node_modules not found
+    echo Please run npm install first to install dependencies.
+    echo.
+    pause
+    exit /b 1
+)
+
+REM Run Node.js script to compare and update dependencies.txt
+node "%~dp0update-nodejs-dependencies.js"
+
+if errorlevel 1 (
+    echo.
+    echo [ERROR] Update failed
+    echo.
+    pause
+    exit /b 1
+) else (
+    echo.
+    echo [OK] Update completed
+    echo.
+    pause
+)

+ 242 - 0
nodejs/dependences/x64/update-nodejs-dependencies.js

@@ -0,0 +1,242 @@
+#!/usr/bin/env node
+/**
+ * Node.js 依赖列表更新脚本
+ * 功能:对比 node_modules 中已安装的包和 dependencies.txt,如果不一致则更新 dependencies.txt
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+// 获取脚本所在目录和项目根目录
+const scriptDir = __dirname;
+const projectRoot = path.dirname(path.dirname(path.dirname(scriptDir)));
+const dependenciesFile = path.join(scriptDir, 'dependencies.txt');
+const nodeModulesPath = path.join(projectRoot, 'node_modules');
+
+// 颜色输出函数
+const colors = {
+    reset: '\x1b[0m',
+    red: '\x1b[31m',
+    green: '\x1b[32m',
+    yellow: '\x1b[33m',
+    cyan: '\x1b[36m',
+    white: '\x1b[37m'
+};
+
+function log(message, color = 'reset') {
+    console.log(`${colors[color]}${message}${colors.reset}`);
+}
+
+/**
+ * 获取 node_modules 中已安装的包列表(包含版本号)
+ */
+function getInstalledPackages() {
+    const installedPackages = {};
+    
+    if (!fs.existsSync(nodeModulesPath)) {
+        log('[ERROR] node_modules not found', 'red');
+        return null;
+    }
+    
+    try {
+        const entries = fs.readdirSync(nodeModulesPath, { withFileTypes: true });
+        
+        for (const entry of entries) {
+            if (entry.isDirectory()) {
+                const packageName = entry.name;
+                
+                // 跳过特殊目录
+                if (packageName.startsWith('.') || packageName === 'node_modules') {
+                    continue;
+                }
+                
+                // 处理普通包
+                const packageJsonPath = path.join(nodeModulesPath, packageName, 'package.json');
+                if (fs.existsSync(packageJsonPath)) {
+                    try {
+                        const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
+                        if (pkg.name && pkg.version) {
+                            installedPackages[pkg.name.toLowerCase()] = `${pkg.name}==${pkg.version}`;
+                        }
+                    } catch (error) {
+                        // 忽略解析错误
+                    }
+                }
+                
+                // 处理 scoped 包(如 @babel/core)
+                if (packageName.startsWith('@')) {
+                    try {
+                        const scopedPath = path.join(nodeModulesPath, packageName);
+                        const scopedEntries = fs.readdirSync(scopedPath, { withFileTypes: true });
+                        for (const scopedEntry of scopedEntries) {
+                            if (scopedEntry.isDirectory()) {
+                                const scopedPackageJsonPath = path.join(scopedPath, scopedEntry.name, 'package.json');
+                                if (fs.existsSync(scopedPackageJsonPath)) {
+                                    try {
+                                        const pkg = JSON.parse(fs.readFileSync(scopedPackageJsonPath, 'utf-8'));
+                                        if (pkg.name && pkg.version) {
+                                            installedPackages[pkg.name.toLowerCase()] = `${pkg.name}==${pkg.version}`;
+                                        }
+                                    } catch (error) {
+                                        // 忽略解析错误
+                                    }
+                                }
+                            }
+                        }
+                    } catch (error) {
+                        // 忽略错误
+                    }
+                }
+            }
+        }
+    } catch (error) {
+        log(`[ERROR] Failed to read node_modules: ${error.message}`, 'red');
+        return null;
+    }
+    
+    return installedPackages;
+}
+
+/**
+ * 读取 dependencies.txt 中的包列表
+ */
+function readDependenciesFile() {
+    if (!fs.existsSync(dependenciesFile)) {
+        log(`[WARN] ${dependenciesFile} not found, will create new one`, 'yellow');
+        return {};
+    }
+    
+    const packages = {};
+    try {
+        const content = fs.readFileSync(dependenciesFile, 'utf-8');
+        const lines = content.split('\n');
+        
+        for (const line of lines) {
+            const trimmed = line.trim();
+            if (trimmed && !trimmed.startsWith('#')) {
+                // 提取包名(支持 ==, >=, <=, >, <, ~= 等版本操作符)
+                if (trimmed.includes('==')) {
+                    const parts = trimmed.split('==', 2);
+                    packages[parts[0].trim().toLowerCase()] = trimmed;
+                } else {
+                    // 如果没有版本号,使用整行
+                    const pkgName = trimmed.split('>=')[0].split('<=')[0].split('>')[0].split('<')[0].split('~=')[0].trim();
+                    packages[pkgName.toLowerCase()] = trimmed;
+                }
+            }
+        }
+    } catch (error) {
+        log(`[ERROR] Failed to read dependencies.txt: ${error.message}`, 'red');
+        return {};
+    }
+    
+    return packages;
+}
+
+/**
+ * 对比并更新 dependencies.txt
+ */
+function compareAndUpdate() {
+    log('Comparing installed packages with dependencies.txt...', 'cyan');
+    log('='.repeat(60), 'cyan');
+    
+    // 获取已安装的包
+    const installedPackages = getInstalledPackages();
+    if (installedPackages === null) {
+        process.exit(1);
+    }
+    
+    // 读取 dependencies.txt 中的包
+    const filePackages = readDependenciesFile();
+    
+    // 对比差异
+    const installedSet = new Set(Object.keys(installedPackages));
+    const fileSet = new Set(Object.keys(filePackages));
+    
+    const addedPackages = [];
+    const removedPackages = [];
+    const changedPackages = [];
+    
+    // 检查新增的包
+    for (const pkgName of installedSet) {
+        if (!fileSet.has(pkgName)) {
+            addedPackages.push(pkgName);
+        }
+    }
+    
+    // 检查删除的包
+    for (const pkgName of fileSet) {
+        if (!installedSet.has(pkgName)) {
+            removedPackages.push(pkgName);
+        }
+    }
+    
+    // 检查版本变化
+    for (const pkgName of installedSet) {
+        if (fileSet.has(pkgName)) {
+            if (installedPackages[pkgName] !== filePackages[pkgName]) {
+                changedPackages.push(pkgName);
+            }
+        }
+    }
+    
+    // 显示差异
+    if (addedPackages.length > 0) {
+        log(`\n[+] Added packages (${addedPackages.length}):`, 'green');
+        addedPackages.sort().forEach(pkg => {
+            log(`  + ${installedPackages[pkg]}`, 'green');
+        });
+    }
+    
+    if (removedPackages.length > 0) {
+        log(`\n[-] Removed packages (${removedPackages.length}):`, 'red');
+        removedPackages.sort().forEach(pkg => {
+            log(`  - ${filePackages[pkg]}`, 'red');
+        });
+    }
+    
+    if (changedPackages.length > 0) {
+        log(`\n[~] Changed packages (${changedPackages.length}):`, 'yellow');
+        changedPackages.sort().forEach(pkg => {
+            log(`  ~ ${filePackages[pkg]} -> ${installedPackages[pkg]}`, 'yellow');
+        });
+    }
+    
+    // 判断是否需要更新
+    if (addedPackages.length === 0 && removedPackages.length === 0 && changedPackages.length === 0) {
+        log(`\n[OK] dependencies.txt is up to date`, 'green');
+        log(`     Total packages: ${Object.keys(installedPackages).length}`, 'green');
+        return true;
+    }
+    
+    // 更新 dependencies.txt
+    log(`\nUpdating ${dependenciesFile}...`, 'cyan');
+    
+    // 获取所有已安装的包(按名称排序)
+    const allPackages = Object.values(installedPackages).sort();
+    
+    // 写入文件(UTF-8 编码)
+    try {
+        fs.writeFileSync(dependenciesFile, allPackages.join('\n') + '\n', 'utf-8');
+        log(`[OK] ${dependenciesFile} updated successfully`, 'green');
+        log(`     Total packages: ${allPackages.length}`, 'green');
+        return true;
+    } catch (error) {
+        log(`[ERROR] Failed to write dependencies.txt: ${error.message}`, 'red');
+        return false;
+    }
+}
+
+/**
+ * 主函数
+ */
+function main() {
+    if (!compareAndUpdate()) {
+        process.exit(1);
+    }
+    process.exit(0);
+}
+
+if (require.main === module) {
+    main();
+}

+ 4 - 44
nodejs/run-process.js

@@ -21,43 +21,12 @@ const ADB_PORT = 5555
 
 
 const ipListJson = process.argv[2]
 const ipListJson = process.argv[2]
 const scriptName = process.argv[3]
 const scriptName = process.argv[3]
-if (!scriptName) {
-  process.stderr.write('run-process: missing scriptName\n')
-  process.exit(1)
-}
-
 const folderPath = path.resolve(path.join(staticRoot, 'process', scriptName))
 const folderPath = path.resolve(path.join(staticRoot, 'process', scriptName))
-
-function writeLog(folderPath, message) {
-  const logFile = path.join(folderPath, 'log.txt')
-  if (!fs.existsSync(folderPath)) fs.mkdirSync(folderPath, { recursive: true })
-  const ts = new Date().toISOString().replace('T', ' ').slice(0, 19)
-  fs.appendFileSync(logFile, `[${ts}] ${message}\n`)
-}
-
-try {
-  writeLog(folderPath, `[run-process] start scriptName=${scriptName} folderPath=${folderPath}`)
-} catch (e) {
-  process.stderr.write(`run-process: writeLog failed ${e.message}\n`)
-}
-
-let ipList
-try {
-  ipList = JSON.parse(ipListJson || '[]')
-} catch (e) {
-  writeLog(folderPath, `[错误] 解析 IP 列表失败: ${e.message}`)
-  process.exit(1)
-}
+const ipList = JSON.parse(ipListJson)
 
 
 let shouldStop = false
 let shouldStop = false
-let actions
-try {
-  const { parseWorkflow } = require('./ef-compiler/ef-compiler.js')
-  actions = parseWorkflow(JSON.parse(fs.readFileSync(path.join(folderPath, 'process.json'), 'utf8'))).actions
-} catch (e) {
-  writeLog(folderPath, `[错误] 加载 process.json 失败: ${e.message}`)
-  process.exit(1)
-}
+const { parseWorkflow } = require('./ef-compiler/ef-compiler.js')
+const actions = parseWorkflow(JSON.parse(fs.readFileSync(path.join(folderPath, 'process.json'), 'utf8'))).actions
 
 
 const { executeActionSequence } = require('./ef-compiler/ef-compiler.js')
 const { executeActionSequence } = require('./ef-compiler/ef-compiler.js')
 const resolution = { width: 1080, height: 1920 }
 const resolution = { width: 1080, height: 1920 }
@@ -70,20 +39,11 @@ function ensureDeviceConnected(ip, port) {
 
 
 /** 启动执行:遍历 ip 列表并异步执行脚本;任一台失败则停止全部并返回失败设备 IP */
 /** 启动执行:遍历 ip 列表并异步执行脚本;任一台失败则停止全部并返回失败设备 IP */
 async function start() {
 async function start() {
-  if (!ipList || ipList.length === 0) {
-    writeLog(folderPath, '[run-process] 无设备,退出')
-    process.stdout.write(JSON.stringify({ success: true, results: [] }) + '\n')
-    process.exit(0)
-  }
-  writeLog(folderPath, `[run-process] 开始执行 ${ipList.length} 台设备: ${ipList.join(', ')}`)
   let failedIp = null
   let failedIp = null
   const runOne = async (ip) => {
   const runOne = async (ip) => {
     if (shouldStop) return { ip, success: false, stopped: true }
     if (shouldStop) return { ip, success: false, stopped: true }
     const connected = ensureDeviceConnected(ip, ADB_PORT)
     const connected = ensureDeviceConnected(ip, ADB_PORT)
-    if (!connected) {
-      writeLog(folderPath, `[run-process] ${ip}:${ADB_PORT} 连接未就绪,跳过`)
-      return { ip, success: false }
-    }
+    if (!connected) return { ip, success: false }
     const result = await executeActionSequence(actions, `${ip}:${ADB_PORT}`, folderPath, resolution, 1000, null, () => shouldStop)
     const result = await executeActionSequence(actions, `${ip}:${ADB_PORT}`, folderPath, resolution, 1000, null, () => shouldStop)
     if (!result.success) {
     if (!result.success) {
       if (!failedIp) { failedIp = ip; shouldStop = true }
       if (!failedIp) { failedIp = ip; shouldStop = true }

+ 0 - 1
python/x64/environment.txt

@@ -1,4 +1,3 @@
-coloredlogs==15.0.1
 flatbuffers==25.12.19
 flatbuffers==25.12.19
 humanfriendly==10.0
 humanfriendly==10.0
 mpmath==1.3.0
 mpmath==1.3.0

+ 1 - 1
src/page/home.jsx

@@ -28,7 +28,7 @@ function Home() {
     hintView.setShowCallback(setShowHint)
     hintView.setShowCallback(setShowHint)
     alertView.setShowCallback(setShowAlert)
     alertView.setShowCallback(setShowAlert)
     comfirmView.setShowCallback(setShowComfirm)
     comfirmView.setShowCallback(setShowComfirm)
-    // navigate('/page/visual-code')
+    navigate('/page/visual-code')
   }, [navigate])
   }, [navigate])
   
   
   return (
   return (

+ 2 - 0
src/page/process/process-item/process-item.js

@@ -44,10 +44,12 @@ class ProcessItemClass {
       hintView.show()
       hintView.show()
       return
       return
     }
     }
+
     this._stopped = false
     this._stopped = false
     this.setIsRunning(true)
     this.setIsRunning(true)
     this._lastRunParams = [JSON.stringify(selectedDevices), this.processInfo.name]
     this._lastRunParams = [JSON.stringify(selectedDevices), this.processInfo.name]
     setExecutionStatus('green')
     setExecutionStatus('green')
+
     window.electronAPI.runNodejsScript('run-process', ...this._lastRunParams)
     window.electronAPI.runNodejsScript('run-process', ...this._lastRunParams)
       .then((res) => {
       .then((res) => {
         if (this._stopped) return
         if (this._stopped) return

+ 95 - 0
src/page/visual_code/code_canvas/code-canvas.js

@@ -0,0 +1,95 @@
+import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'
+import CodeCanvasView from './code-canvas.jsx'
+
+const CHUNK_MARGIN = 1
+
+/** 可拖拽画布:chunk 制,只渲染视口+边距内的块,视觉上无限 */
+function CodeCanvas({ show }) {
+  const [translate, setTranslate] = useState({ x: 0, y: 0 })
+  const [dragStart, setDragStart] = useState(null)
+  const [containerSize, setContainerSize] = useState({ width: 0, height: 0 })
+  const containerRef = useRef(null)
+
+  useEffect(() => {
+    if (!containerRef.current) return
+    const el = containerRef.current
+    const observer = new ResizeObserver((entries) => {
+      const { width, height } = entries[0].contentRect
+      setContainerSize({ width, height })
+    })
+    observer.observe(el)
+    return () => observer.disconnect()
+  }, [show])
+
+  const handleMouseDown = useCallback((e) => {
+    e.preventDefault()
+    setDragStart({ x: e.clientX, y: e.clientY, tx: translate.x, ty: translate.y })
+  }, [translate])
+
+  useEffect(() => {
+    if (!dragStart) return
+    const onMove = (e) => {
+      setTranslate({
+        x: dragStart.tx + e.clientX - dragStart.x,
+        y: dragStart.ty + e.clientY - dragStart.y,
+      })
+    }
+    const onUp = () => setDragStart(null)
+    document.addEventListener('mousemove', onMove)
+    document.addEventListener('mouseup', onUp)
+    return () => {
+      document.removeEventListener('mousemove', onMove)
+      document.removeEventListener('mouseup', onUp)
+    }
+  }, [dragStart])
+
+  const { panTransform, visibleChunks, boxPosition } = useMemo(() => {
+    const { width: cw, height: ch } = containerSize
+    if (!cw || !ch) {
+      return { panTransform: { x: 0, y: 0 }, visibleChunks: [], boxPosition: null }
+    }
+
+    const panX = cw / 2 + translate.x
+    const panY = ch / 2 + translate.y
+    const viewportLeft = -panX
+    const viewportTop = -panY
+    const viewportRight = -panX + cw
+    const viewportBottom = -panY + ch
+
+    const minChunkX = Math.floor(viewportLeft / cw) - CHUNK_MARGIN
+    const maxChunkX = Math.ceil(viewportRight / cw) + CHUNK_MARGIN
+    const minChunkY = Math.floor(viewportTop / ch) - CHUNK_MARGIN
+    const maxChunkY = Math.ceil(viewportBottom / ch) + CHUNK_MARGIN
+
+    const chunks = []
+    for (let j = minChunkY; j <= maxChunkY; j++) {
+      for (let i = minChunkX; i <= maxChunkX; i++) {
+        chunks.push({ i, j })
+      }
+    }
+
+    const boxW = cw * 0.1
+    const boxH = ch * 0.1
+    const boxLeft = -boxW / 2
+    const boxTop = -boxH / 2
+
+    return {
+      panTransform: { x: panX, y: panY },
+      visibleChunks: chunks,
+      boxPosition: { left: boxLeft, top: boxTop, width: boxW, height: boxH },
+    }
+  }, [containerSize, translate])
+
+  return React.createElement(CodeCanvasView, {
+    show,
+    panTransform,
+    visibleChunks,
+    boxPosition,
+    containerSize,
+    isDragging: !!dragStart,
+    onMouseDown: handleMouseDown,
+    containerRef,
+  })
+}
+
+export default CodeCanvas

+ 36 - 7
src/page/visual_code/code_canvas/code-canvas.jsx

@@ -1,16 +1,45 @@
 import React from 'react'
 import React from 'react'
 import './code-canvas.scss'
 import './code-canvas.scss'
 
 
-function CodeCanvas({ show }) {
-  if (!show) {
-    return null
-  }
+function CodeCanvasView({ show, panTransform, visibleChunks, boxPosition, containerSize, isDragging, onMouseDown, containerRef }) {
+  if (!show) return null
 
 
-  return (
-    <div className="code-canvas-container">
+  const chunkW = containerSize?.width || 0
+  const chunkH = containerSize?.height || 0
 
 
+  return (
+    <div className="code-canvas-container" ref={containerRef}>
+      <div
+        className={`pan-layer ${isDragging ? 'dragging' : ''}`}
+        style={{ transform: `translate(${panTransform.x}px, ${panTransform.y}px)` }}
+        onMouseDown={onMouseDown}
+      >
+        {visibleChunks.map(({ i, j }) => (
+          <div
+            key={`${i}-${j}`}
+            className="chunk"
+            style={{
+              left: i * chunkW,
+              top: j * chunkH,
+              width: chunkW,
+              height: chunkH,
+            }}
+          />
+        ))}
+        {boxPosition && (
+          <div
+            className="box"
+            style={{
+              left: boxPosition.left,
+              top: boxPosition.top,
+              width: boxPosition.width,
+              height: boxPosition.height,
+            }}
+          />
+        )}
+      </div>
     </div>
     </div>
   )
   )
 }
 }
 
 
-export default CodeCanvas
+export default CodeCanvasView

+ 31 - 2
src/page/visual_code/code_canvas/code-canvas.scss

@@ -1,5 +1,34 @@
 .code-canvas-container {
 .code-canvas-container {
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
-  background-color: #000000f1;
-}
+  overflow: hidden;
+  container-type: size;
+
+  .pan-layer {
+    position: absolute;
+    left: 0;
+    top: 0;
+    cursor: grab;
+    user-select: none;
+
+    &.dragging {
+      cursor: grabbing;
+    }
+
+    .chunk {
+      position: absolute;
+      left: 0;
+      top: 0;
+      background-color: #353333e8;
+      background-image:
+        linear-gradient(rgba(100, 100, 100, 1) 1px, transparent 1px),
+        linear-gradient(90deg, rgba(100, 100, 100, 1) 1px, transparent 1px);
+      background-size: 5cqw 5cqh;
+    }
+
+    .box {
+      position: absolute;
+      background-color: #e5ff00;
+    }
+  }
+}

+ 1 - 1
src/page/visual_code/visual_code.jsx

@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'
 import './visual_code.scss'
 import './visual_code.scss'
 import TopBar from './top_bar/top-bar.jsx'
 import TopBar from './top_bar/top-bar.jsx'
 import SideBar from './side_bar/side-bar.jsx'
 import SideBar from './side_bar/side-bar.jsx'
-import CodeCanvas from './code_canvas/code-canvas.jsx'
+import CodeCanvas from './code_canvas/code-canvas.js'
 
 
 function VisualCode() {
 function VisualCode() {
   return (
   return (

+ 6 - 0
src/page/visual_code/visual_code.scss

@@ -8,6 +8,8 @@
     width: 100%;
     width: 100%;
     height: 10%;
     height: 10%;
     min-height: 40px;
     min-height: 40px;
+    position: relative;
+    z-index: 2;
   }
   }
   .visual-code-content {
   .visual-code-content {
     width: 100%;
     width: 100%;
@@ -19,10 +21,14 @@
     .side-bar {
     .side-bar {
       width: 20%;
       width: 20%;
       height: 100%;
       height: 100%;
+      position: relative;
+      z-index: 2;
     }
     }
     .code-canvas {
     .code-canvas {
       width: 80%;
       width: 80%;
       height: 100%;
       height: 100%;
+      position: relative;
+      z-index: 1;
     }
     }
   }
   }
 }
 }

+ 4 - 1
static/device_list.json

@@ -1,3 +1,6 @@
 {
 {
-  "devices": []
+  "devices": [
+    "192.168.2.5",
+    "192.168.1.24"
+  ]
 }
 }