Selaa lähdekoodia

多设备使用

yichael 2 kuukautta sitten
vanhempi
sitoutus
60852182b9

+ 3 - 12
bat-tool/adb-connect-test/adb-connect.bat

@@ -1,14 +1,5 @@
 @echo off
-chcp 65001 >nul
-title ADB Connect Test
 cd /d "%~dp0"
-node adb-connect.js 192.168.2.5 5555
-if errorlevel 1 (
-    echo.
-    pause
-    exit /b 1
-) else (
-    echo.
-    pause
-    exit /b 0
-)
+node adb-connect.js
+
+pause

+ 11 - 43
bat-tool/adb-connect-test/adb-connect.js

@@ -1,51 +1,19 @@
 #!/usr/bin/env node
 const { execSync } = require('child_process')
-const path = require('path')
 
-// 从配置文件读取 ADB 路径
-const config = require(path.join(__dirname, '..', '..', 'configs', 'config.js'))
-const projectRoot = path.resolve(__dirname, '..', '..')
-const adbPath = config.adbPath?.path 
-  ? path.resolve(projectRoot, config.adbPath.path) 
-  : path.join(projectRoot, 'lib', 'scrcpy-adb', 'adb.exe')
+const config = require(process.cwd() + '/../../configs/config.js')
+const adbPath = config.adbPath?.path
 
-// Get device IP and port from command line arguments
-const deviceIp = process.argv[2] || '192.168.2.5'
-const devicePort = process.argv[3] || '5555'
+const deviceIp = '192.168.0.101'
+const devicePort = '5555'
 
-if (!deviceIp) {
-  console.error('Usage: node adb-connect.js <ip> [port]')
-  console.error('Example: node adb-connect.js 192.168.2.5 5555')
-  process.exit(1)
+/** Run adb connect and return whether connected. */
+function connect() {
+  const out = execSync(`"${adbPath}" connect ${deviceIp}:${devicePort}`, { encoding: 'utf-8' }).trim()
+  return out.includes('connected') || out.includes('already connected')
 }
 
-console.log(`Connecting to ${deviceIp}:${devicePort}...`)
-console.log('========================================')
+const ok = connect()
+console.log(ok ? `${deviceIp}:${devicePort}  Connected` : `${deviceIp}:${devicePort}  Connect failed`)
+process.exit(ok ? 0 : 1)
 
-try {
-  const connectCommand = `"${adbPath}" connect ${deviceIp}:${devicePort}`
-  const output = execSync(connectCommand, { encoding: 'utf-8' })
-  const result = output.trim()
-  console.log(result)
-  
-  const isConnected = result.includes('connected') || result.includes('already connected')
-  
-  console.log('========================================')
-  if (isConnected) {
-    console.log('[OK] Device connected successfully')
-    console.log('')
-    process.exit(0)
-  } else {
-    console.log('[ERROR] Failed to connect to device')
-    console.log('')
-    process.exit(1)
-  }
-} catch (error) {
-  console.log('========================================')
-  console.log('[ERROR] Failed to connect to device')
-  console.log('')
-  if (error.message) {
-    console.error(error.message)
-  }
-  process.exit(1)
-}

+ 1 - 1
configs/config.js

@@ -35,6 +35,6 @@ module.exports = {
 
   // ADB 路径配置(相对于项目根目录)
   adbPath: {
-    path: 'lib/scrcpy-adb/adb.exe' // ADB 可执行文件路径(相对路径)
+    path: path.join(projectRoot, 'lib/scrcpy-adb/adb.exe')// ADB 可执行文件路径(相对路径)
   }
 }

+ 60 - 0
doc/JSX组件继承说明.md

@@ -0,0 +1,60 @@
+# JSX 组件继承说明
+
+JSX 用组合代替继承:子组件直接复用父组件,通过 props 传差异。
+
+## 最简例子
+
+**parent.jsx**
+```jsx
+function Parent({ title, children }) {
+  return <div><h3>{title}</h3>{children}</div>
+}
+```
+
+**child.jsx**
+```jsx
+import Parent from './parent.jsx'
+function Child() {
+  return <Parent title="开始" />
+}
+```
+
+Child 等价于 `<div><h3>开始</h3></div>`,只是复用 Parent 的壳。
+
+**main.jsx 调用**
+```jsx
+import Child from './child.jsx'
+function Main() {
+  return <Child />
+}
+```
+
+## 虚函数等价:传函数当 props
+
+父定义调用点,子传入实现,效果等同 OOP 的虚函数:
+
+```jsx
+// parent.jsx:定义调用点
+function Parent({ title, onRenderContent }) {
+  return (
+    <div>
+      <h3>{title}</h3>
+      {onRenderContent ? onRenderContent() : null}
+    </div>
+  )
+}
+
+// child.jsx:传入自己的实现
+import Parent from './parent.jsx'
+function Child() {
+  return (
+    <Parent title="开始" onRenderContent={() => <div>我自定义的内容</div>} />
+  )
+}
+
+// main.jsx 调用
+import Child from './child.jsx'
+function Main() {
+  return <Child />
+}
+```

+ 54 - 0
doc/可插拔组件模式.md

@@ -0,0 +1,54 @@
+# 可插拔组件模式
+
+一个主实体 + 可拔插的 Component。Unity、Unreal、Vue 等都用类似思路:主模块容纳组件,按需挂载或卸载。
+
+## 思路
+
+- **主模块**:一个空壳,只负责布局和容纳组件
+- **Component**:独立功能块,通过配置决定挂哪些、不挂哪些
+
+## 最简实现
+
+### 1. 主模块(main.jsx)
+
+```jsx
+// main.jsx
+function Main({ components = [] }) {
+  return (
+    <div className="main">
+      {components.map((Comp, i) => Comp && <Comp key={i} />)}
+    </div>
+  )
+}
+```
+
+### 2. 可拔插的 Component(component.jsx)
+
+```jsx
+// component.jsx
+function HealthBar() {
+  return <div className="health-bar">HP: 100</div>
+}
+
+function DragHandle() {
+  return <div className="drag-handle">⋮⋮</div>
+}
+```
+
+### 3. 按需挂载
+
+```jsx
+import Main from './main.jsx'
+import { HealthBar, DragHandle } from './component.jsx'
+
+// 只挂拖拽
+<Main components={[DragHandle]} />
+
+// 挂两个
+<Main components={[DragHandle, HealthBar]} />
+
+// 不挂任何组件
+<Main components={[]} />
+```
+
+插拔 = 改 `components` 数组即可。

+ 18 - 1
electron/main.js

@@ -16,6 +16,20 @@ if (!fs.existsSync(staticDir)) {
 const config = require(path.join(unpackedRoot, 'configs', 'config.js'))
 const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged
 
+// Set Content-Security-Policy for packaged app (removes Electron security warning in production)
+if (!isDev) {
+  const { session } = require('electron')
+  session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
+    const url = details.url || ''
+    if (url.startsWith('file://')) {
+      const csp = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
+      callback({ responseHeaders: { ...details.responseHeaders, 'Content-Security-Policy': [csp] } })
+    } else {
+      callback({ responseHeaders: details.responseHeaders })
+    }
+  })
+}
+
 // 打包后:userData 放在 exe 同目录 UserData,实现「开包即用」— 整包复制到任意电脑即可使用
 // 开发时:使用临时目录,避免污染项目
 if (process.platform === 'win32') {
@@ -70,7 +84,10 @@ function createWindow() {
     const vitePort = config.vite?.port || 5173
     const viteHost = config.vite?.host || 'localhost'
     startupLog(`Loading Vite dev server at http://${viteHost}:${vitePort}`)
-    mainWindow.loadURL(`http://${viteHost}:${vitePort}`)
+    // Clear cache before dev load to avoid ERR_CACHE_READ_FAILURE with react-refresh
+    mainWindow.webContents.session.clearCache().then(() => {
+      mainWindow.loadURL(`http://${viteHost}:${vitePort}`)
+    })
 
     // 根据配置文件决定是否打开调试侧边栏
     if (config.devTools.enabled) {

+ 19 - 10
nodejs/adb/screenshot.js

@@ -15,7 +15,21 @@ const staticRoot = process.env.STATIC_ROOT ? path.resolve(process.env.STATIC_ROO
 const pidFile = path.join(staticRoot, 'scrcpy-pid.json')
 
 const action = process.argv[2]
-const pidArg = process.argv[3]
+const ipArg = process.argv[3]
+const portArg = process.argv[4]
+// 支持两种传参:1) ip, port 分开  2) ip:port 合并(无默认 port,只用传入值)
+let ip, port
+if (portArg !== undefined && portArg !== '') {
+  ip = ipArg
+  port = portArg
+} else if (ipArg && String(ipArg).includes(':')) {
+  const parts = String(ipArg).split(':')
+  ip = parts[0] || ''
+  port = parts[1] || ''
+} else {
+  ip = ipArg || ''
+  port = portArg || ''
+}
 
 /** 终止指定 PID 的进程 */
 function killPidIfRunning(pid) {
@@ -39,11 +53,6 @@ function isDeviceIp(val) {
   return val && !/^\d+$/.test(val) && /[\d.]/.test(val)
 }
 
-/** 根据传入的 IP 得到 adb/scrcpy 设备选择器,无线默认 5555 */
-function toDeviceSelector(ipOrSelector) {
-  return ipOrSelector.includes(':') ? ipOrSelector : `${ipOrSelector}:5555`
-}
-
 /** 启动 scrcpy:先 adb connect(若有 IP),再用 scrcpy-noconsole.bat(start "" scrcpy.exe %*)无控制台启动 */
 function startScrcpy() {
   if (fs.existsSync(pidFile)) {
@@ -52,12 +61,12 @@ function startScrcpy() {
   }
 
   let deviceSelector = ''
-  if (isDeviceIp(pidArg)) {
-    deviceSelector = toDeviceSelector(pidArg)
+  if (ip && port && isDeviceIp(ip)) {
+    deviceSelector = `${ip}:${port}`
     execSync(`"${adbPath}" connect ${deviceSelector}`, { encoding: 'utf-8', cwd: scrcpyDir })
   }
 
-  const serial = deviceSelector || (pidArg && !/^\d+$/.test(pidArg) ? pidArg : '')
+  const serial = deviceSelector
   const args = serial
     ? ['--pause-on-exit=if-error', '-s', serial]
     : ['-e', '--pause-on-exit=if-error']
@@ -101,7 +110,7 @@ switch (action) {
   default:
     console.log(JSON.stringify({ 
       success: false, 
-      error: 'Usage: node screenshot.js [start|stop] [pid|deviceIp]' 
+      error: 'Usage: node screenshot.js [start|stop] [ip] [port]' 
     }))
     process.exit(1)
 }

+ 52 - 8
nodejs/run-process.js

@@ -22,29 +22,70 @@ const ADB_PORT = 5555
 const ipListJson = process.argv[2]
 const scriptName = process.argv[3]
 const folderPath = path.resolve(path.join(staticRoot, 'process', scriptName))
+const logFilePath = path.join(folderPath, 'log.txt')
 const ipList = JSON.parse(ipListJson)
 
 let shouldStop = false
+
+function appendLog(text) {
+  try {
+    fs.appendFileSync(logFilePath, text, 'utf8')
+  } catch (e) {}
+}
+
+function logLine(msg) {
+  const line = `[${new Date().toISOString()}] ${msg}\n`
+  process.stdout.write(line)
+  appendLog(line)
+}
 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 resolution = { width: 1080, height: 1920 }
 
-/** 对指定 IP 执行 adb connect,确保设备在列表中后再跑流程 */
-function ensureDeviceConnected(ip, port) {
-  const out = execSync(`"${adbPath}" connect ${ip}:${port}`, { encoding: 'utf-8' }).trim()
-  return out.includes('connected') || out.includes('already connected')
+function sleep(ms) {
+  return new Promise((resolve) => setTimeout(resolve, ms))
+}
+
+/** 对指定 IP 执行 adb connect,与 bat-tool/adb-connect-test 行为一致:可重试、短延迟,确保设备就绪 */
+async function ensureDeviceConnected(ip, port, logLineFn) {
+  const deviceId = `${ip}:${port}`
+  const maxTries = 3
+  const delayMs = 2000
+  for (let i = 0; i < maxTries; i++) {
+    try {
+      const out = execSync(`"${adbPath}" connect ${deviceId}`, { encoding: 'utf-8' }).trim()
+      if (out.includes('connected') || out.includes('already connected')) return true
+    } catch (e) {
+      // execSync 抛错(如 adb 未找到)时不再重试
+      if (logLineFn) logLineFn(`Connect attempt ${i + 1}/${maxTries}: ${(e.stderr || e.message || '').trim() || 'failed'}`)
+    }
+    if (i < maxTries - 1) {
+      if (logLineFn) logLineFn(`Wait ${delayMs / 1000}s, retry connect ${deviceId} ...`)
+      await sleep(delayMs)
+    }
+  }
+  return false
 }
 
 /** 启动执行:遍历 ip 列表并异步执行脚本;任一台失败则停止全部并返回失败设备 IP */
 async function start() {
+  appendLog(`\n========== Run ${new Date().toISOString()} ==========\n`)
+  logLine(`Process "${scriptName}" start, devices: ${ipList.length}`)
   let failedIp = null
   const runOne = async (ip) => {
     if (shouldStop) return { ip, success: false, stopped: true }
-    const connected = ensureDeviceConnected(ip, ADB_PORT)
-    if (!connected) return { ip, success: false }
-    const result = await executeActionSequence(actions, `${ip}:${ADB_PORT}`, folderPath, resolution, 1000, null, () => shouldStop)
+    const deviceId = ip.includes(':') ? ip : `${ip}:${ADB_PORT}`
+    const [deviceIp, devicePort] = deviceId.includes(':') ? deviceId.split(':') : [ip, ADB_PORT]
+    logLine(`Connecting ${deviceId} ...`)
+    const connected = await ensureDeviceConnected(deviceIp, devicePort, logLine)
+    if (!connected) {
+      logLine(`Failed to connect ${deviceId}`)
+      return { ip, success: false }
+    }
+    logLine(`Running on ${deviceId}`)
+    const result = await executeActionSequence(actions, deviceId, folderPath, resolution, 1000, null, () => shouldStop)
     if (!result.success) {
       if (!failedIp) { failedIp = ip; shouldStop = true }
     }
@@ -56,7 +97,10 @@ async function start() {
   const output = failedIp
     ? { success: false, failedIp, results }
     : { success: true, results }
-  process.stdout.write(JSON.stringify(output) + '\n')
+  logLine(failedIp ? `Process finished with failed device: ${failedIp}` : 'Process finished successfully.')
+  const resultLine = JSON.stringify(output) + '\n'
+  process.stdout.write(resultLine)
+  appendLog(resultLine)
   process.exit(failedIp ? 1 : 0)
 }
 

+ 7 - 1
src/page/device/connect-item/connect-item.js

@@ -5,6 +5,9 @@ class ConnectItemClass {
     /** 初始化连接项:IP、预览状态与回调 */
     async init(ipAddress, isPreviewing, setIsPreviewing, onPreview, onRemove) {
         this.ipAddress = ipAddress
+        const [ip, port] = (ipAddress || '').includes(':') ? ipAddress.split(':') : [ipAddress || '', '5555']
+        this.ip = ip
+        this.port = port
         this.is_previewing = isPreviewing || false
         this.setIsPreviewing = setIsPreviewing
         this.onRemoveCallback = onRemove
@@ -24,7 +27,10 @@ class ConnectItemClass {
 
     /** 触发 scrcpy 预览:先尝试停止,成功则返回 stop;否则启动并返回 start(主进程可能提前 resolve,exitCode 为 null 按成功处理) */
     async triggerPreview() {
-        const startResult = await window.electronAPI.runNodejsScript('adb/screenshot', 'start', this.ipAddress)
+        const deviceStr = `${this.ip}:${this.port}`
+        const result = await window.electronAPI.runNodejsScript('adb/screenshot', 'start', deviceStr)
+        if (result?.success) this.setIsPreviewing?.(true)
+        return result
     }
 
     /** 从 adb 移除该设备,返回是否成功 */

+ 2 - 1
src/page/device/connect-item/connect-item.jsx

@@ -4,6 +4,7 @@ import { ConnectItemClass } from './connect-item.js'
 
 // 执行状态灯:grey 未执行/已停止 | green 执行中 | red 执行失败
 function ConnectItem({ ipAddress, selected=false, executionStatus = null, onSelect, onRemove, onPreview }) {
+  const [ip, port] = (ipAddress || '').includes(':') ? ipAddress.split(':') : [ipAddress || '', '5555']
   const connectItemClassRef = useRef(null)
   const [isPreviewing, setIsPreviewing] = useState(false)
 
@@ -51,7 +52,7 @@ function ConnectItem({ ipAddress, selected=false, executionStatus = null, onSele
       
       <div className="item-container">
         
-        <div className="ip-address">{ipAddress}</div>
+        <div className="ip-address">{ip}<br />({port})</div>
       
         <div className="btn-area-container">
           

+ 30 - 15
src/page/device/device.js

@@ -38,12 +38,14 @@ export function setExecutionStatus(color, failedIps = []) {
 class DeviceClass {
     constructor() {}
 
-    async init(setDeviceList, inputValue, setSelectedDevice, setInputValue)
+    async init(setDeviceList, inputValue, portValue, setSelectedDevice, setInputValue, setPortValue)
     {
         this.setDeviceList = setDeviceList
         this.inputValue = inputValue
+        this.portValue = portValue
         this.setSelectedDevice = setSelectedDevice
         this.setInputValue = setInputValue
+        this.setPortValue = setPortValue
         this.count_ip_x = 0
         this.count_ip_y = 0
 
@@ -99,21 +101,28 @@ class DeviceClass {
     }
     
     async onAddDevice() {
-        const ip = this.inputValue; 
-        const ipList = await window.electronAPI.runNodejsScript('json-parser', 'read', 'device_list.json')
+        const ip = (this.inputValue || '').trim()
+        const port = (this.portValue || '').trim()
+        const deviceStr = ip && port ? `${ip}:${port}` : ip || ''
+        if (!deviceStr) {
+            hintView.setContent('请填写 IP 和端口')
+            hintView.show()
+            return
+        }
 
+        const ipList = await window.electronAPI.runNodejsScript('json-parser', 'read', 'device_list.json')
         const jsonData = JSON.parse(ipList.stdout)
         const devices = jsonData.data.devices
-        if (devices.includes(ip)) {
-      
+        if (devices.includes(deviceStr)) {
             hintView.setContent('设备已存在')
             hintView.show()
             return
-        }     
+        }
 
-        this.setSelectedDevice(prev => [...(prev || []), ip])
-        await this.addDevice(ip, this.currentDeviceList || [])
+        this.setSelectedDevice(prev => [...(prev || []), deviceStr])
+        await this.addDevice(deviceStr, this.currentDeviceList || [])
         this.setInputValue?.('192.168.')
+        this.setPortValue?.('5555')
     }
 
     async addDevice(ip, currentList = []) {
@@ -128,13 +137,19 @@ class DeviceClass {
 
         comfirmView.onConfirm = async () => {
             comfirmView.hide()
-            
-            let newArr = null
-            this.setDeviceList(prev => {
-                newArr = [...prev.filter(deviceIp => deviceIp !== ip)]
-                return newArr
-            })
-            await window.electronAPI.runNodejsScript('json-parser', 'update', 'device_list.json', JSON.stringify({devices: newArr}))
+            const currentList = Array.isArray(this.currentDeviceList) ? this.currentDeviceList : []
+            const newArr = currentList.filter(deviceIp => deviceIp !== ip)
+            this.setDeviceList(newArr)
+            try {
+                const result = await window.electronAPI.runNodejsScript('json-parser', 'update', 'device_list.json', JSON.stringify({ devices: newArr }))
+                if (result.exitCode !== 0) {
+                    hintView.setContent('删除设备失败')
+                    hintView.show()
+                }
+            } catch (e) {
+                hintView.setContent('删除设备失败')
+                hintView.show()
+            }
         }
         comfirmView.onCancel = () => {
             comfirmView.hide()

+ 15 - 7
src/page/device/device.jsx

@@ -9,6 +9,7 @@ function Device({ show }) {
   const [deviceList, setDeviceList] = useState([])
   const deviceClass = useRef(null)
   const [inputValue, setInputValue] = useState('192.168.')
+  const [portValue, setPortValue] = useState('5555')
   const [selectedDevices, setSelectedDevices] = useState([])  // 数组,记录所有选中的 device IP
   const [executionStatus, setExecutionStatus] = useState(getExecutionStatus())
   if (!show) {
@@ -28,7 +29,7 @@ function Device({ show }) {
   useEffect(() => {
     if (!deviceClass.current) {
       deviceClass.current = new DeviceClass()
-      deviceClass.current.init(setDeviceList, inputValue, setSelectedDevices, setInputValue)
+      deviceClass.current.init(setDeviceList, inputValue, portValue, setSelectedDevices, setInputValue, setPortValue)
     }
   }, [])
 
@@ -36,6 +37,10 @@ function Device({ show }) {
     if (deviceClass.current) deviceClass.current.inputValue = inputValue
   }, [inputValue])
 
+  useEffect(() => {
+    if (deviceClass.current) deviceClass.current.portValue = portValue
+  }, [portValue])
+
   useEffect(() => {
     if (deviceClass.current) deviceClass.current.currentDeviceList = deviceList
   }, [deviceList])
@@ -50,12 +55,6 @@ function Device({ show }) {
         {/* 更新设备列表 */}
         <div className="device-update">
           <div className="device-update-title">设备列表</div>
-          {/* <div className="device-update-btn">
-            <UpdateBtn
-              onClick={(e, self) => deviceClass.current?.onRefresh(e, self)}
-              title="Refresh device list"
-            />
-          </div> */}
           <div className="enable-wirless-connect-btn" onClick={() => deviceClass.current?.onEnableWirlessConnect()}>激活</div>
         </div>
 
@@ -84,6 +83,15 @@ function Device({ show }) {
               onKeyDown={(e) => { if (e.key === 'Enter') { deviceClass.current?.onAddDevice() } }}
             />
           </div>
+          <div className="port-input">
+            <input 
+              type="text" 
+              placeholder="请输入设备端口" 
+              value={portValue}
+              onChange={(e) => setPortValue(e.target.value)}
+              onKeyDown={(e) => { if (e.key === 'Enter') { deviceClass.current?.onAddDevice() } }}
+            />
+          </div>
           <div className="add-btn" onClick={() => deviceClass.current?.onAddDevice()}>
             <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
               <path d="M12 5V19M5 12H19" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>

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

@@ -57,10 +57,25 @@
     @include flex-row-between;
 
     .ip-input {
-      width: 80%;
+      width: 60%;
       height: 50%;
       @include flex-center;
       
+      input {
+        outline: none;
+        border: none;
+        text-align: center;
+        width: 90%;
+        height: 100%;
+
+        border: 1px solid #000000;
+      }
+    }
+
+    .port-input {
+      width: 30%;
+      height: 50%;
+      @include flex-center;
       
       input {
         outline: none;
@@ -72,6 +87,7 @@
         border: 1px solid #000000;
       }
     }
+
     .add-btn {
       width: 20%;
       height: 100%;

+ 1 - 1
src/page/home.jsx

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

+ 43 - 34
src/page/visual_code/code_canvas/code-canvas.js

@@ -1,61 +1,33 @@
 import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'
 import CodeCanvasView from './code-canvas.jsx'
 
+/** 视口外多渲染的 chunk 层数,接近边界时提前出现,避免白边 */
 const CHUNK_MARGIN = 1
 
-/** 可拖拽画布:chunk 制,只渲染视口+边距内的块,视觉上无限 */
+/** 可拖拽画布: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])
-
+  /** ① 每轮渲染先算:平移量、可见 chunk、box 位置(供 return 用) */
   const { panTransform, visibleChunks, boxPosition } = useMemo(() => {
     const { width: cw, height: ch } = containerSize
     if (!cw || !ch) {
       return { panTransform: { x: 0, y: 0 }, visibleChunks: [], boxPosition: null }
     }
 
+    /** 平移层 transform:世界中心 (0,0) 对准容器中心,再叠加拖拽位移 */
     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
 
+    /** 与视口相交的 chunk 索引范围(含 CHUNK_MARGIN 边距) */
     const minChunkX = Math.floor(viewportLeft / cw) - CHUNK_MARGIN
     const maxChunkX = Math.ceil(viewportRight / cw) + CHUNK_MARGIN
     const minChunkY = Math.floor(viewportTop / ch) - CHUNK_MARGIN
@@ -68,6 +40,7 @@ function CodeCanvas({ show }) {
       }
     }
 
+    /** box 固定在世界原点 (0,0),尺寸为容器 10%,居中故取负半宽高为 left/top */
     const boxW = cw * 0.1
     const boxH = ch * 0.1
     const boxLeft = -boxW / 2
@@ -80,6 +53,42 @@ function CodeCanvas({ show }) {
     }
   }, [containerSize, translate])
 
+  /** ② 鼠标按下时执行:记录拖拽起点与当前 translate */
+  const handleMouseDown = useCallback((e) => {
+    e.preventDefault()
+    setDragStart({ x: e.clientX, y: e.clientY, tx: translate.x, ty: translate.y })
+  }, [translate])
+
+  /** ③ 挂载/展示变化后执行:ResizeObserver 同步容器宽高 */
+  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])
+
+  /** ④ dragStart 变化后执行:在 document 上监听 mousemove/mouseup,更新 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])
+
   return React.createElement(CodeCanvasView, {
     show,
     panTransform,

+ 2 - 9
src/page/visual_code/code_canvas/code-canvas.jsx

@@ -1,5 +1,6 @@
 import React from 'react'
 import './code-canvas.scss'
+import Begin from '../node/begin/begin.jsx'
 
 function CodeCanvasView({ show, panTransform, visibleChunks, boxPosition, containerSize, isDragging, onMouseDown, containerRef }) {
   if (!show) return null
@@ -27,15 +28,7 @@ function CodeCanvasView({ show, panTransform, visibleChunks, boxPosition, contai
           />
         ))}
         {boxPosition && (
-          <div
-            className="box"
-            style={{
-              left: boxPosition.left,
-              top: boxPosition.top,
-              width: boxPosition.width,
-              height: boxPosition.height,
-            }}
-          />
+          <Begin />
         )}
       </div>
     </div>

+ 0 - 5
src/page/visual_code/code_canvas/code-canvas.scss

@@ -25,10 +25,5 @@
         linear-gradient(90deg, rgba(100, 100, 100, 1) 1px, transparent 1px);
       background-size: 5cqw 5cqh;
     }
-
-    .box {
-      position: absolute;
-      background-color: #e5ff00;
-    }
   }
 }

+ 0 - 0
src/page/visual_code/node/begin/begin.js


+ 20 - 0
src/page/visual_code/node/begin/begin.jsx

@@ -0,0 +1,20 @@
+import React from 'react'
+import './begin.scss'
+import Node from '../node.jsx'
+
+function begin() {
+  return (
+    <Node>
+      <div className="begin-container">
+        <div className="node-title">Begin</div>
+          <div className="process-arrow">
+          <svg className="process-arrow-icon" viewBox="0 0 24 24">
+            <path className="process-arrow-path" d="M8 5l10 7-10 7V5z" />
+          </svg>
+        </div>
+      </div>
+    </Node>
+  )
+}
+
+export default begin

+ 48 - 0
src/page/visual_code/node/begin/begin.scss

@@ -0,0 +1,48 @@
+
+.begin-container
+{
+    width: 20vw;
+    height: 20vh;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: flex-start;
+
+    .node-title
+    {
+        width: 100%;
+        height: 30%;
+
+        display: flex;
+        align-items: center;
+        justify-content: center;
+
+        background-color: #00bbff;
+        border: 1px solid #ffffff;
+    }
+
+    .process-arrow
+    {
+        width: 100%;
+        height: 30%;
+
+        display: flex;
+        align-items: center;
+        justify-content: flex-end;
+
+        .process-arrow-icon
+        {
+            width: 80%;
+            height: auto;  // 按 viewBox 比例保持宽高比
+        }
+
+        .process-arrow-path
+        {
+            stroke: rgba(255, 255, 255, 0.8);  // 外轮廓线颜色
+            fill: rgba(0, 0, 0, 0);            // 内部填充,透明=空心;改 alpha>0 实现实心
+            stroke-width: 1.5;
+            stroke-linecap: round;
+            stroke-linejoin: round;
+        }
+    }
+}

+ 26 - 0
src/page/visual_code/node/node.js

@@ -0,0 +1,26 @@
+import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'
+import node from './node.jsx'
+
+
+function CodeCanvas({ show }) {
+ 
+
+  /** ③ 挂载/展示变化后执行:ResizeObserver 同步容器宽高 */
+  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])
+
+
+  return React.createElement(node, {
+   title: 'node',
+  })
+}
+
+export default node

+ 14 - 0
src/page/visual_code/node/node.jsx

@@ -0,0 +1,14 @@
+import React from 'react'
+import './node.scss'
+
+function node({children}) {
+  return (
+    <div className="node-container">
+      <div className="node-frame">
+         {children}
+      </div>
+    </div>
+  )
+}
+
+export default node

+ 14 - 0
src/page/visual_code/node/node.scss

@@ -0,0 +1,14 @@
+.node-container
+{
+    position: absolute;
+    width: fit-content;
+    height: fit-content;
+
+    .node-frame
+    {
+        width: fit-content;
+        height: fit-content;
+        background-color: #3b3838e3;
+        border: 1px solid #ffffff;
+    }
+}

+ 2 - 2
static/device_list.json

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

BIN
static/process/RedNoteAIThumbsUp/resources/点赞按钮_未点赞.png


BIN
static/process/RedNoteAIThumbsUp/resources/点赞按钮_未点赞2.png


BIN
static/process/RedNoteBrowsingAndThumbsUp/resources/点赞按钮_未点赞.png


BIN
static/process/RedNoteBrowsingAndThumbsUp/resources/点赞按钮_未点赞2.png


+ 1 - 1
static/scrcpy-pid.json

@@ -1 +1 @@
-{"pid":5068}
+{"pid":23144}