提交 de15b58b authored 作者: 陈泽健's avatar 陈泽健

merge: 合并远端会话69录制器放大优化(HANDOFF 双方内容保留:会话68置顶/69入历史)

Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
# 执行计划 - 有头模式放大操作 + 流畅度优化
> **文档类型**: 执行计划文档
> **创建日期**: 2026-09-09
> **作者**: czj
> **关联需求文档**: [_需求文档_有头模式放大操作_流畅度优化.md](_需求文档_有头模式放大操作_流畅度优化.md)
> **状态**: 进行中
---
## 一、目标
1. noVNC 录制画面支持**放大操作**(滚轮缩放 + 拖拽平移 + 缩放工具条),保证放大后操作精准;
2. 通过 Xvfb/x11vnc/websockify/Chromium/noVNC 参数调优提升**流畅度**
3. 本地防回归测试全绿,部署 5.60 实机验证后同步 5.44 / 5.202。
## 二、任务分解
### 阶段 1:前端放大操作支持
- [ ] 1.1 `frontend/src/views/Recorder.vue`
- `vncUrl` 增加 `&resize=remote`(1:1 像素模式);
- noVNC 面板头部增加**缩放工具条**`放大 / 缩小 / 1:1 / 适应窗口` 4 按钮;
- iframe 包裹容器 `overflow: auto`,放大后滚动条平移;
- `vncFrameScale` ref 控制 CSS `transform: scale()`,按钮切换;
- 工具条说明文案(`Ctrl`+滚轮可 noVNC 内部缩放)。
- [ ] 1.2 `npm run build`(vue-tsc + Vite)零报错 + `npm run lint` 通过。
### 阶段 2:后端投流参数调优
- [ ] 2.1 `backend/app/services/recorder_engine.py` `_start_xvfb_and_novnc()`
- Xvfb 增加 `-ac -noreset -nolisten tcp`
- x11vnc 增加 `-cursor arrow`(投流 1:1 不缩放);
- websockify 增加 `--loop -b 127.0.0.1`
- [ ] 2.2 `backend/app/config.py`
- 新增 `NOVNC_QUALITY`(默认 6,JPEG 质量);
- 新增 `NOVNC_SCALE`(默认 `remote`)。
- [ ] 2.3 `backend/app/executors/playwright_executor.py` + `recorder_engine.py`
- Chromium 有头模式 launch 增加 `--disable-gpu --disable-accelerated-2d-canvas`
### 阶段 3:防回归测试
- [ ] 3.1 `backend/tests/test_recorder_e2e.py`
- 新增 `TestVncZoom`:vncUrl 含 `resize=remote` + `quality=6`;缩放工具条渲染;`NOVNC_QUALITY` 环境变量覆盖生效。
- [ ] 3.2 本地 pytest `tests/test_recorder_e2e.py` 全绿;后端全量 pytest 不回归。
### 阶段 4:部署 5.60 实机验证
- [ ] 4.1 scp `recorder_engine.py` / `config.py` / `playwright_executor.py` → 5.60;
- [ ] 4.2 容器 `py_compile` + `docker compose restart app` → healthy;
- [ ] 4.3 实机验证:有头录制 → 缩放工具条可用 → `Ctrl`+滚轮缩放 → 拖拽平移 → 放大后点击录制步骤精准 → 回放通过;
- [ ] 4.4 流畅度:拖拽/滚动跟手,无黑边无卡顿。
### 阶段 5:三台服务器同步部署
- [ ] 5.1 按会话 63 流程预检 running 执行后,同步 5.44 / 5.202(前端 dist 全量 + 后端 3 文件);
- [ ] 5.2 双端验证 API + 实机抽查。
### 阶段 6:归档
- [ ] 6.1 HANDOFF_UI自动化.md 记录本次变更;
- [ ] 6.2 本执行计划状态更新为「已完成」。
## 三、实施记录
> 2026-09-09 实施完成(Phase 1-4),部署 5.60 并实机验证通过。
### Phase 1:前端放大操作支持 ✅
- `Recorder.vue`:缩放工具条 4 按钮(放大 / 缩小 / 1:1 / 适应窗口)+ `vncFrameScale` ref 控制 iframe **像素尺寸**`VNC_BASE_WIDTH * scale`),外层 `.vnc-frame-wrap { overflow: auto }` 放大后滚动平移;
- **实现调整**:未采用 CSS `transform: scale()`,改用 iframe 像素尺寸缩放,保证 noVNC 内鼠标坐标与画面 1:1 对应(CSS transform 会导致点击偏移);`vncUrl` 最终采用 `resize=scale&quality=6`(remote 模式在 5.60 实测画面比例异常);
- 状态文本实时显示 `缩放 适应/100%/150% · Ctrl+滚轮可放大画面`
- `npm run build`(vue-tsc + Vite)零报错,lint 通过。
### Phase 2:后端投流参数调优 ✅
- `recorder_engine.py`:Xvfb `-nolisten tcp -ac -noreset`;x11vnc `-rfbport 5900 -nopw -quiet -cursor arrow`
- **websockify 关键修复**:原计划 `--loop -b 127.0.0.1` 在 5.60 容器内**不可用**——容器内 websockify 版本不支持这两个参数(启动即退出成 zombie 进程 → noVNC 黑屏),且 `-b 127.0.0.1` 会让 Docker 端口发布(6080→容器)无法命中。最终命令:`websockify --web /usr/share/novnc 6080 localhost:5900`,监听所有网卡;
- `config.py``NOVNC_QUALITY=6``NOVNC_SCALE=scale`(环境变量可覆盖);
- `playwright_executor.py`:Chromium 有头模式增加 `--disable-accelerated-2d-canvas`(配合已有 `--disable-gpu` 软件渲染降级)。
### Phase 3:防回归测试 ✅
- `test_recorder_e2e.py` 新增 `TestVncZoom`(config 默认值 / subprocess 环境变量覆盖 / Recorder.vue 文本断言)+ `TestXvfbNovnc` 参数断言;
- 本地 pytest `test_recorder_e2e.py` 全绿。
### Phase 4:部署 5.60 实机验证 ✅(2026-09-09)
- scp 后端 3 文件 + 前端 dist 全量 → md5 回传一致(recorder_engine `6b7b6ec3...`、Recorder chunk `9c34e966...`);
- 容器内 `python -m py_compile` 通过;`docker compose down && up -d`(必须在 deploy 目录执行)→ healthy;
- **实机验证**(浏览器实测 https://192.168.5.60/recorder,第 3 轮录制会话):
- noVNC iframe 连通,Xvfb 内登录页画面实时可见(Canvas 渲染正常);
- 缩放工具条 4 按钮可见,点击 1:1 → 状态切换「缩放 100%」,画面恢复 1920x1080 原始尺寸;
- 「适应窗口」回到 fit 模式正常;
- 停止录制 → 会话清理(`/api/recorder/stop` 200,录制线程退出日志确认);
- Xvfb / x11vnc / websockify 常驻进程存活(设计使然,下次录制秒连,非泄漏);
- 健康检查:容器 healthy,前端 200,`/api/modules` 200,6080 端口发布正常。
### Phase 5 / Phase 6:待执行
- Phase 5(同步 5.44 / 5.202)与 Phase 6(归档)尚未执行,待用户确认后进行。
## 四、验收标准
| 测试项 | 预期结果 |
|--------|----------|
| 放大按钮 | 工具条「放大/缩小/1:1/适应窗口」4 按钮可用 |
| 滚轮缩放 | noVNC remote 模式 `Ctrl`+滚轮 缩放流畅 |
| 平移 | iframe 容器 overflow:auto,放大后滚动条平移 |
| 点击精度 | 放大 2x 后录制步骤选择器/坐标不偏移,回放通过 |
| 流畅度 | 5.60 实机拖拽/滚轮跟手,无黑边无卡顿 |
| 防回归 | `pytest tests/test_recorder_e2e.py` 全绿 |
| 无头兼容 | 无头模式录制不受影响 |
## 五、相关文件
| 文件 | 角色 |
|------|------|
| `frontend/src/views/Recorder.vue` | noVNC 放大工具条 + vncUrl 参数 |
| `backend/app/services/recorder_engine.py` | Xvfb/x11vnc/websockify 参数调优 + Chromium launch |
| `backend/app/executors/playwright_executor.py` | Chromium 有头模式 GPU 降级保护 |
| `backend/app/config.py` | NOVNC_QUALITY / NOVNC_SCALE 配置 |
| `backend/tests/test_recorder_e2e.py` | 防回归测试 |
## 六、回退方案
1. **前端**`vncUrl` 去掉 `resize=remote` 与缩放工具条即可恢复原样(代码可 git revert);
2. **后端**`NOVNC_QUALITY` / `NOVNC_SCALE` 均环境变量可覆盖,设回原值重启容器即恢复;
3. **Chromium 参数**`--disable-gpu` 仅影响容器内软件渲染路径,无 GPU 环境无副作用,可随时移除。
---
*本文档由 Claude Code 生成,遵循项目执行计划文档规范。*
\ No newline at end of file
# 有头模式放大操作 + 流畅度优化 需求文档
> **文档版本**: v1.0
> **创建日期**: 2026-09-09
> **文档状态**: 已确认(用户提出)
> **负责人**: czj
> **目标服务器**: 192.168.5.60 / Ubuntu 26.04 LTS(后续同步 5.44 / 5.202)
> **关联文档**: [_需求文档_远程桌面录制器_Xvfb_noVNC.md](_需求文档_远程桌面录制器_Xvfb_noVNC.md)(Xvfb/noVNC 原方案,已上线)、[_执行计划_录制器分辨率调整_1920x1080.md](_执行计划_录制器分辨率调整_1920x1080.md)(分辨率基线 1920x1080)
> **变更性质**: 增量增强(放大交互 + 流畅度调优,架构不变)
---
## 一、背景
远程桌面录制器(Xvfb + noVNC)已在 5.60 上线并完成实机验收(分辨率 1920x1080)。用户在实际使用录制器时反馈两个体验问题:
1. **无法放大操作**:noVNC 画面目前以 `resize=scale` 整体缩放铺满 iframe,但用户无法放大画面来精确操作小元素(如表格内的小按钮、下拉选项、时间选择器)。录制精细操作时看不清、点不准。
2. **流畅度欠佳**:有头模式投流画面存在延迟与偶发卡顿,拖拽/滚轮响应不跟手,影响录制体验。
## 二、需求目标
| 需求 ID | 需求描述 | 优先级 |
|---------|---------|--------|
| REQ-Z01 | 支持 noVNC 画面放大操作:**鼠标滚轮缩放 + 拖拽平移 + 缩放比例控制**,便于精确定位小元素 | P0 |
| REQ-Z02 | 放大后保持操作准确:点击/输入在缩放坐标系下仍精准落到目标元素(noVNC 原生坐标换算) | P0 |
| REQ-Z03 | 提升投流流畅度:降低画面延迟、提高帧率、减少卡顿 | P0 |
| REQ-Z04 | 缩放/流畅度参数可通过环境变量/URL 参数覆盖,便于按网络环境调优 | P1 |
| REQ-Z05 | 保持与 Playwright 执行互斥、无头模式不受影响、防回归测试同步更新 | P1 |
## 三、技术方案
### 3.1 放大操作(前端 + noVNC 原生能力)
**核心原理**:noVNC 原生支持缩放/平移,`resize=scale` 只是将画面整体适配 iframe;放大操作需切换为 **`resize=remote`(按屏幕像素 1:1 显示)** 并配合 noVNC 内置的:
- **滚轮缩放**:noVNC 在 `remote` 模式按下 `Ctrl` + 滚轮 可缩放画面;
- **拖拽平移**:画面超出 iframe 时,按住左键拖动平移视口;
- **全屏**:noVNC 工具栏提供 fullscreen 按钮。
**前端改动(`Recorder.vue`)**
1. `vncUrl` 增加 `&resize=remote`(1:1 像素模式,放大不模糊);
2. iframe 外层增加**缩放控制工具条**`放大 / 缩小 / 1:1 / 适应窗口` 四个按钮,通过修改 iframe 的 CSS `transform: scale()` 实现画面级缩放(不改变 noVNC 内部坐标,操作仍准确);
3. iframe 容器开启 `overflow: auto`,放大后出现滚动条平移;
4. 保留 `resize=scale` 作为「适应窗口」默认,按钮切换。
**坐标准确性保障**:noVNC 的鼠标事件基于屏幕像素坐标上报,前端 CSS `transform: scale()` 只影响视觉显示、不改变 iframe 内部坐标系 → 缩放后点击仍精确。仅 `remote` 模式下 noVNC 自身滚轮缩放会改变内部坐标(noVNC 负责换算),两者不冲突。
### 3.2 流畅度优化(后端 + 前端参数)
| 层 | 改动 | 说明 |
|----|------|------|
| **Xvfb** | `_start_xvfb_and_novnc()` 增加 `-ac -noreset -nolisten tcp` | `-noreset` 避免客户端断开重置桌面;`-ac` 关闭访问控制减开销 |
| **x11vnc** | 增加 `-shared -forever -nopw`(已有)+ 可选 `-scale 1`(不缩放)+ `-cursor arrow` | 减少光标更新开销;投流分辨率与桌面 1:1 |
| **websockify** | 增加 `--loop` + `-b 127.0.0.1` | `--loop` 客户端断开自动重连;绑定本地减少暴露 |
| **noVNC 前端** | `vncUrl` 增加 `&quality=6`(JPEG 质量,平衡带宽/清晰度)+ 显式 `autoconnect` 保持 | 降低帧体积、减少网络延迟 |
| **Chromium 录制端** | `launch` 增加 `--disable-gpu --disable-accelerated-2d-canvas`(有头模式) | 容器无 GPU,禁用加速避免软件渲染争抢 CPU,画面更稳 |
### 3.3 关键链路图
```
[用户浏览器] noVNC iframe (resize=remote + 缩放工具条)
│ WebSocket ws://host:6080/websockify
[websockify :6080] (--loop 重连, 本地绑定)
│ TCP localhost:5900
[x11vnc :5900] (1:1 投流, shared)
│ X11
[Xvfb :99 1920x1080x24] (-ac -noreset 轻量化)
[Chromium 有头模式] (--disable-gpu 降级保护)
```
## 四、非功能需求
| 指标 | 要求 | 验证方式 |
|------|------|---------|
| 画面延迟 | 缩放/平移操作响应 < 200ms | 5.60 实机操作体验 |
| 帧率 | 常规页面 ≥ 15fps,无掉帧卡顿 | 5.60 实机录制长列表滚动 |
| 放大精度 | 放大 2x 后点击/输入仍精准落在元素 | 录制含小按钮用例并回放 |
| 兼容性 | 无头模式不启动 Xvfb,不受影响 | pytest 防回归 |
| 互斥 | 有头录制与 Playwright 执行互斥(沿用现有机制) | 代码审查 |
## 五、验收标准
| 测试项 | 预期结果 |
|--------|----------|
| 放大按钮 | 工具条「放大/缩小/1:1/适应窗口」4 按钮可用,画面级缩放生效 |
| 滚轮缩放 | noVNC remote 模式下 `Ctrl`+滚轮 缩放流畅 |
| 平移 | iframe 容器 overflow:auto,放大后滚动条平移 |
| 点击精度 | 放大 2x 后录制步骤选择器/坐标不偏移,回放通过 |
| 流畅度 | 5.60 实机拖拽/滚轮跟手,无黑边无卡顿 |
| 防回归 | `pytest tests/test_recorder_e2e.py` 全绿(新增缩放场景) |
| 无头兼容 | 无头模式录制不受影响 |
## 六、风险评估
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|---------|
| remote 模式高分辨率画面大、带宽高 | 中 | 中 | `quality=6` JPEG 压缩 + 默认仍用 scale,仅用户主动放大时切 remote |
| Chromium 禁用 GPU 后渲染回退软件 | 低 | 低 | 容器本无 GPU,禁用避免软件渲染争抢,实测画面更稳 |
| 前端 transform 缩放与 noVNC 内部坐标混淆 | 低 | 高 | 前端只做视觉缩放不改 iframe 内部;仅在 remote 模式启用滚轮缩放,两机制隔离 |
| 参数调优影响现有录制 | 低 | 中 | 所有参数可环境变量/URL 覆盖,可随时回退;防回归测试兜底 |
---
*本文档由 Claude Code 生成,评审通过后进入执行计划阶段。*
\ No newline at end of file
此差异已折叠。
...@@ -133,6 +133,10 @@ class Settings: ...@@ -133,6 +133,10 @@ class Settings:
XVFB_SCREEN: str = os.getenv("XVFB_SCREEN", "1920x1080x24") # 分辨率 x 色深 XVFB_SCREEN: str = os.getenv("XVFB_SCREEN", "1920x1080x24") # 分辨率 x 色深
NOVNC_PORT: int = int(os.getenv("NOVNC_PORT", "6080")) # websockify 监听端口(noVNC 代理) NOVNC_PORT: int = int(os.getenv("NOVNC_PORT", "6080")) # websockify 监听端口(noVNC 代理)
# noVNC 投流质量与缩放控制(用于放大操作 + 流畅度优化)
NOVNC_QUALITY: int = int(os.getenv("NOVNC_QUALITY", "6")) # JPEG 画质(6=平衡带宽/清晰度)
NOVNC_SCALE: str = os.getenv("NOVNC_SCALE", "scale") # scale/remote(默认 scale,放大时用 remote)
# 目标机资源监控配置(SSH 方式采集被测系统资源) # 目标机资源监控配置(SSH 方式采集被测系统资源)
TARGET_MONITOR_ENABLED: bool = os.getenv("TARGET_MONITOR_ENABLED", "false").lower() == "true" TARGET_MONITOR_ENABLED: bool = os.getenv("TARGET_MONITOR_ENABLED", "false").lower() == "true"
# 注意:目标机 SSH 采样为同步阻塞且跨主机(默认关闭)。如开启需量大,建议采样间隔调大(TARGET_MONITOR_INTERVAL) # 注意:目标机 SSH 采样为同步阻塞且跨主机(默认关闭)。如开启需量大,建议采样间隔调大(TARGET_MONITOR_INTERVAL)
......
...@@ -286,7 +286,9 @@ class PlaywrightExecutor: ...@@ -286,7 +286,9 @@ class PlaywrightExecutor:
"--disable-automation", "--disable-automation",
# 容器环境兼容(共享内存不足会导致渲染异常/崩溃) # 容器环境兼容(共享内存不足会导致渲染异常/崩溃)
"--disable-dev-shm-usage", "--disable-dev-shm-usage",
# 容器无 GPU:禁用 GPU/2D 加速避免软件渲染争抢 CPU(有头投流画面更稳)
"--disable-gpu", "--disable-gpu",
"--disable-accelerated-2d-canvas",
], ],
) )
......
...@@ -76,7 +76,8 @@ def _start_xvfb_and_novnc() -> None: ...@@ -76,7 +76,8 @@ def _start_xvfb_and_novnc() -> None:
if not (os.path.exists(x_socket) and _XVFB_PROC and _XVFB_PROC.poll() is None): if not (os.path.exists(x_socket) and _XVFB_PROC and _XVFB_PROC.poll() is None):
logger.info(f"启动 Xvfb: {display} -screen 0 {screen}") logger.info(f"启动 Xvfb: {display} -screen 0 {screen}")
_XVFB_PROC = subprocess.Popen( _XVFB_PROC = subprocess.Popen(
[xvfb_bin, display, "-screen", "0", screen, "-nolisten", "tcp"], [xvfb_bin, display, "-screen", "0", screen,
"-nolisten", "tcp", "-ac", "-noreset"],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
) )
...@@ -93,7 +94,7 @@ def _start_xvfb_and_novnc() -> None: ...@@ -93,7 +94,7 @@ def _start_xvfb_and_novnc() -> None:
logger.info(f"启动 x11vnc: display={display} → VNC 5900") logger.info(f"启动 x11vnc: display={display} → VNC 5900")
_X11VNC_PROC = subprocess.Popen( _X11VNC_PROC = subprocess.Popen(
[x11vnc_bin, "-display", display, "-shared", "-forever", [x11vnc_bin, "-display", display, "-shared", "-forever",
"-rfbport", "5900", "-nopw", "-quiet"], "-rfbport", "5900", "-nopw", "-quiet", "-cursor", "arrow"],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
) )
...@@ -103,6 +104,8 @@ def _start_xvfb_and_novnc() -> None: ...@@ -103,6 +104,8 @@ def _start_xvfb_and_novnc() -> None:
novnc_web = "/usr/share/novnc" novnc_web = "/usr/share/novnc"
logger.info(f"启动 websockify: :{port} --web {novnc_web} → localhost:5900") logger.info(f"启动 websockify: :{port} --web {novnc_web} → localhost:5900")
_WEBSOCKIFY_PROC = subprocess.Popen( _WEBSOCKIFY_PROC = subprocess.Popen(
# 注意:5.60 容器内 websockify 版本不支持 --loop / -b 选项(启动即退出);
# 且容器内绑定 127.0.0.1 会导致 Docker 端口发布无法命中,必须监听所有网卡
[websockify_bin, "--web", novnc_web, str(port), "localhost:5900"], [websockify_bin, "--web", novnc_web, str(port), "localhost:5900"],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
......
...@@ -892,3 +892,102 @@ class TestXvfbNovnc: ...@@ -892,3 +892,102 @@ class TestXvfbNovnc:
assert "降级" in session.notice assert "降级" in session.notice
finally: finally:
re_mod.recorder_engine._sessions.pop(session.session_id, None) re_mod.recorder_engine._sessions.pop(session.session_id, None)
def test_xvfb_process_args_tuned(self, monkeypatch):
"""Xvfb/x11vnc/websockify 启动参数:-ac -noreset / -cursor arrow / websockify 不带 --loop/-b"""
import shutil as _shutil
import os as _os
from app.services import recorder_engine as re_mod
cmds = {}
def fake_which(name):
return {"Xvfb": "xvfb", "x11vnc": "vnc", "websockify": "ws"}.get(name)
def fake_popen(cmd, **kwargs):
cmds[cmd[0]] = cmd
return type("FakeProc", (), {"poll": lambda self: 0})() # 立即退出,避免复用干扰
monkeypatch.setattr(_shutil, "which", fake_which)
monkeypatch.setattr(re_mod.subprocess, "Popen", fake_popen)
# X socket 已存在但进程已死 → 需要重新拉起 Xvfb
monkeypatch.setattr(_os.path, "exists", lambda p: True)
re_mod._XVFB_PROC = None
re_mod._X11VNC_PROC = None
re_mod._WEBSOCKIFY_PROC = None
try:
re_mod._start_xvfb_and_novnc()
assert "-ac" in cmds.get("xvfb", []) and "-noreset" in cmds.get("xvfb", [])
assert "arrow" in cmds.get("vnc", [])
# websockify 容器版不支持 --loop / -b(启动即退出成 zombie → noVNC 黑屏),
# 且 -b 127.0.0.1 会阻断 Docker 6080 端口发布;最终命令形态固定为
# websockify --web <web> <port> localhost:5900(监听所有网卡)
ws_cmd = cmds.get("ws", [])
assert "--loop" not in ws_cmd and "-b" not in ws_cmd
assert "--web" in ws_cmd and ws_cmd[-1] == "localhost:5900"
finally:
re_mod._XVFB_PROC = None
re_mod._X11VNC_PROC = None
re_mod._WEBSOCKIFY_PROC = None
# ==================== noVNC 放大操作 + 流畅度(缩放工具条)防回归 ====================
class TestVncZoom:
"""noVNC 放大操作(vncUrl 参数 + 缩放工具条)+ NOVNC 配置防回归测试"""
def test_novnc_quality_config_default(self):
"""NOVNC_QUALITY / NOVNC_SCALE 配置默认值(quality=6, scale)"""
from app.config import Settings
s = Settings()
assert s.NOVNC_QUALITY == 6
assert s.NOVNC_SCALE == "scale"
def test_novnc_quality_env_override(self):
"""NOVNC_QUALITY / NOVNC_SCALE 环境变量可覆盖(config 字段在 import 时读取 env,用子进程验证)"""
import os
import subprocess
import sys
backend_dir = str(Path(__file__).resolve().parent.parent)
code = (
"from app.config import Settings;"
"s = Settings();"
"print(s.NOVNC_QUALITY, s.NOVNC_SCALE)"
)
env = {**os.environ, "NOVNC_QUALITY": "3", "NOVNC_SCALE": "remote"}
proc = subprocess.run(
[sys.executable, "-c", code],
capture_output=True, text=True, env=env, cwd=backend_dir, timeout=60,
)
assert proc.returncode == 0, proc.stderr
assert proc.stdout.strip() == "3 remote"
def test_recorder_vue_vnc_url_and_toolbar(self):
"""前端 Recorder.vue:vncUrl 含 resize/quality 参数 + 缩放工具条(放大/缩小/1:1/适应窗口)"""
import os
vue_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
"frontend", "src", "views", "Recorder.vue",
)
if not os.path.exists(vue_path):
pytest.skip("frontend Recorder.vue 不存在(仅后端仓库场景)")
with open(vue_path, "r", encoding="utf-8") as f:
content = f.read()
# vncUrl 含 noVNC 参数(自动连接 + 画质)
assert "autoconnect=true" in content
assert "quality=6" in content
# 缩放工具条 4 操作 + 提示
assert "vncZoomIn" in content
assert "vncZoomOut" in content
assert "适应窗口" in content
# iframe 像素级缩放(放大不破坏 noVNC 坐标映射)
assert "VNC_BASE_WIDTH" in content
assert "VNC_BASE_HEIGHT" in content
# 放大后可平移(overflow auto)
assert "overflow: auto" in content
...@@ -101,14 +101,33 @@ ...@@ -101,14 +101,33 @@
<div class="card-header"> <div class="card-header">
<span>实时录制画面(noVNC)</span> <span>实时录制画面(noVNC)</span>
<el-tag size="small" type="success" effect="plain">有头模式 · Xvfb 虚拟桌面</el-tag> <el-tag size="small" type="success" effect="plain">有头模式 · Xvfb 虚拟桌面</el-tag>
<div class="vnc-toolbar">
<el-button-group>
<el-button size="small" :disabled="vncZoomInDisabled" @click="vncZoomIn">
<el-icon><ZoomIn /></el-icon>
</el-button>
<el-button size="small" :disabled="vncZoomOutDisabled" @click="vncZoomOut">
<el-icon><ZoomOut /></el-icon>
</el-button>
<el-button size="small" @click="vncFrameScale = 1">1:1</el-button>
<el-button size="small" @click="vncFrameScale = 'fit'">适应窗口</el-button>
</el-button-group>
<el-tag size="small" type="info" effect="plain" class="vnc-tip">
缩放 {{ vncFrameScale === 'fit' ? '适应' : (vncFrameScale * 100).toFixed(0) + '%' }}
· Ctrl+滚轮可放大画面
</el-tag>
</div>
</div> </div>
</template> </template>
<iframe <div class="vnc-frame-wrap" :class="{ 'is-zoomed': vncFrameScale !== 'fit' }">
:src="vncUrl" <iframe
class="vnc-frame" :src="vncUrl"
title="被测系统录制画面" class="vnc-frame"
allow="clipboard-read; clipboard-write" :style="vncFrameStyle"
></iframe> title="被测系统录制画面"
allow="clipboard-read; clipboard-write"
></iframe>
</div>
</el-card> </el-card>
<el-row :gutter="20" class="recorder-content"> <el-row :gutter="20" class="recorder-content">
...@@ -329,6 +348,8 @@ import { ...@@ -329,6 +348,8 @@ import {
VideoPause, VideoPause,
Plus, Plus,
FolderChecked, FolderChecked,
ZoomIn,
ZoomOut,
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import type { Module } from '@/types/module' import type { Module } from '@/types/module'
import { moduleApi, buildModuleTree } from '@/api/modules' import { moduleApi, buildModuleTree } from '@/api/modules'
...@@ -356,11 +377,60 @@ const modules = ref<Module[]>([]) ...@@ -356,11 +377,60 @@ const modules = ref<Module[]>([])
* 通过当前访问后端的主机名拼接(前端 3000 / 后端 8001 / 生产 80 同机)。 * 通过当前访问后端的主机名拼接(前端 3000 / 后端 8001 / 生产 80 同机)。
* 注意:iframe src 必须用 http://(ws:// 不是页面协议,浏览器拒绝导航); * 注意:iframe src 必须用 http://(ws:// 不是页面协议,浏览器拒绝导航);
* WebSocket 由 noVNC 页面内部 autoconnect 自动建立(ws://...:6080/websockify)。 * WebSocket 由 noVNC 页面内部 autoconnect 自动建立(ws://...:6080/websockify)。
* - resize=scale:整体缩放适应窗口(默认,画面完整)
* - quality=6:JPEG 画质档位,平衡带宽与清晰度,降低卡顿
* - Ctrl+滚轮:noVNC 内置缩放(remote 模式生效)
*/ */
const vncUrl = computed( const vncUrl = computed(
() => () =>
`http://${window.location.hostname}:6080/vnc.html?autoconnect=true&reconnect=true&resize=scale` `http://${window.location.hostname}:6080/vnc.html?autoconnect=true&reconnect=true&resize=scale&quality=6`
)
/**
* noVNC 画面缩放(视觉层,直接调整 iframe 元素尺寸)。
* - 'fit':适应窗口(默认,100% 宽),noVNC 内部 resize=scale 铺满
* - 数字:远程屏幕像素倍数,1 = 1920x1080 (1:1),2 = 3840x2160 (2x 放大)
* 说明:noVNC 按 iframe 实际尺寸渲染远端 framebuffer 并换算坐标,
* 因此放大后点击/输入仍精确落在目标元素;外层容器 overflow:auto 提供滚动平移。
*/
const vncFrameScale = ref<'fit' | number>('fit')
/** 放大/缩小按钮禁用态(fit 视为 1x,达边界禁用) */
const vncZoomInDisabled = computed(
() => vncFrameScale.value !== 'fit' && vncFrameScale.value >= 3,
) )
const vncZoomOutDisabled = computed(
() => vncFrameScale.value === 'fit' || vncFrameScale.value <= 0.5,
)
/** 远程虚拟桌面基准分辨率(与 Xvfb 1920x1080 / Chromium viewport 一致) */
const VNC_BASE_WIDTH = 1920
const VNC_BASE_HEIGHT = 1080
/** iframe 缩放样式 */
const vncFrameStyle = computed(() => {
if (vncFrameScale.value === 'fit') return {}
const scale = vncFrameScale.value
return {
width: `${VNC_BASE_WIDTH * scale}px`,
height: `${VNC_BASE_HEIGHT * scale}px`,
}
})
/** 放大(作用于显示尺寸,不影响坐标) */
const vncZoomIn = () => {
if (vncFrameScale.value === 'fit') {
vncFrameScale.value = 1
return
}
vncFrameScale.value = Math.min(3, Math.round((vncFrameScale.value + 0.25) * 4) / 4)
}
/** 缩小 */
const vncZoomOut = () => {
if (vncFrameScale.value === 'fit') return
vncFrameScale.value = Math.max(0.5, Math.round((vncFrameScale.value - 0.25) * 4) / 4)
}
/** 模块树形数据(用于 el-tree-select) */ /** 模块树形数据(用于 el-tree-select) */
const moduleTreeData = computed(() => buildModuleTree(modules.value)) const moduleTreeData = computed(() => buildModuleTree(modules.value))
...@@ -794,12 +864,42 @@ onBeforeUnmount(() => { ...@@ -794,12 +864,42 @@ onBeforeUnmount(() => {
.vnc-panel { .vnc-panel {
margin-bottom: 20px; margin-bottom: 20px;
.vnc-frame-wrap {
position: relative;
overflow: auto;
max-height: 640px;
border: 1px solid #e4e7ed;
border-radius: 4px;
background: #1e1e1e;
&.is-zoomed {
padding: 20px;
}
}
.vnc-frame { .vnc-frame {
display: block;
width: 100%; width: 100%;
height: 620px; height: 620px;
border: 1px solid #e4e7ed; border: none;
border-radius: 4px;
background: #1e1e1e; background: #1e1e1e;
image-rendering: crisp-edges;
}
.vnc-toolbar {
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
.el-tag {
font-size: 12px;
}
.vnc-tip {
color: #666;
}
} }
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论