From c78f17d72fa96b360bec8ca3997c27f615480257 Mon Sep 17 00:00:00 2001 From: butubb <1422726308@qq.com> Date: Wed, 19 Aug 2026 14:13:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9B=91=E6=8E=A7=E5=A4=A7=E5=B1=8F?= =?UTF-8?q?=EF=BC=88/wall=20=E5=85=A8=E5=B1=8F=E6=B7=B1=E8=89=B2=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=AE=A4=E9=A3=8E=E6=A0=BC=EF=BC=9A=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E7=BD=91=E6=A0=BC+=E7=BC=A9=E7=95=A5?= =?UTF-8?q?=E5=9B=BE=E8=BD=AE=E8=AF=A2+=E7=BB=9F=E8=AE=A1+=E6=97=B6?= =?UTF-8?q?=E9=92=9F=EF=BC=8C=E7=BC=A9=E7=95=A5=E5=9B=BE=20360px/q55=20?= =?UTF-8?q?=E6=AF=8F=202.5s=20=E4=B8=80=E5=B8=A7=2020=20=E5=8F=B0=E4=BB=85?= =?UTF-8?q?=20~0.2=20=E6=A0=B8=20CPU=EF=BC=89+=20/api/screen/thumb=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=20+=20=E5=AF=BC=E8=88=AA=E6=A0=8F=E5=A4=A7?= =?UTF-8?q?=E5=B1=8F=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- doc/API.md | 13 +++ templates/admin/monitor.html | 1 + templates/admin/wall.html | 219 +++++++++++++++++++++++++++++++++++ web_server.py | 43 +++++++ 5 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 templates/admin/wall.html diff --git a/README.md b/README.md index b5244d6..f8049cb 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,7 @@ self.set_progress(done=5, total=80, unit="视频", | Tab | 功能 | 可见性 | |-----|------|--------| -| 监控 | 设备状态大屏:在线/离线、型号、运行任务、当前动作、进度条、前台 App、截图、勾选批量操作(亮屏/息屏/停止) | 所有登录用户(设备操作按钮需"设备控制"权限) | +| 监控 | 设备状态大屏:在线/离线、型号、运行任务、当前动作、进度条、前台 App、截图、勾选批量操作(亮屏/息屏/停止);导航栏「📺 大屏」打开全屏监控墙(/wall,缩略图+统计+时钟,挂墙/电视用) | 所有登录用户(设备操作按钮需"设备控制"权限) | | 任务 | 子分栏:任务计划(CRUD/启用停用/立即执行/下次运行时间/**离线设备自动跳过**)、自定义动作(打包复用) | 所有登录用户可看,写操作需"任务管理"权限 | | 分组 | 设备分组管理:创建/编辑/删除分组 | 所有登录用户可看,写操作需"任务管理"权限 | | 日志 | 实时日志查看:按模块切换(core/task/web/action) | 需"日志查看"权限 | diff --git a/doc/API.md b/doc/API.md index dbb29b6..6cc3614 100644 --- a/doc/API.md +++ b/doc/API.md @@ -642,6 +642,19 @@ IP:5555 设备添加后立即尝试 adb connect,并后台采集型号(getpro `?serial=xxx` 指定设备;浏览器 `` 直接渲染,客户端断开自动停止。数据源 u2(atx-agent minicap,约 3-6 帧/秒)。 +### GET /api/screen/thumb + +大屏缩略图:单张 JPEG(360px 宽,质量 55)。**权限**:`devices`。 + +`?serial=xxx` 指定设备。监控大屏按设备每 ~2.5s 轮询一帧(页面不可见时暂停); +一次性请求(非流),与任务并发安全(与任务截图同走 u2 minicap)。 + +### GET /wall + +监控大屏页面(全屏深色控制室风格,登录后可访问):设备卡片网格(缩略图/型号/ +状态/当前动作/进度)、顶部统计与时钟;状态每 5s 刷新、缩略图每 2.5s 轮询。 +20 台设备整体开销约 0.2 核 CPU + 100KB/s 带宽,普通电脑无压力。 + ### POST /api/screen/tap 点击设备屏幕。**权限**:`devices`。 diff --git a/templates/admin/monitor.html b/templates/admin/monitor.html index dce3a42..2e397db 100644 --- a/templates/admin/monitor.html +++ b/templates/admin/monitor.html @@ -320,6 +320,7 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
... + 📺 大屏 退出
diff --git a/templates/admin/wall.html b/templates/admin/wall.html new file mode 100644 index 0000000..d29f377 --- /dev/null +++ b/templates/admin/wall.html @@ -0,0 +1,219 @@ + + + + + +设备监控大屏 + + + +
+
设备监控大屏
+
+
--:--:--
+
+
+ + + + diff --git a/web_server.py b/web_server.py index 0408dd8..a4ac1e6 100644 --- a/web_server.py +++ b/web_server.py @@ -187,6 +187,19 @@ def index(): return resp +@app.route("/wall") +@login_required +def wall(): + """监控大屏(独立全屏页面,供挂墙/电视展示)。 + + 深色控制室风格;设备缩略图按需轮询(每设备 ~2.5s 一帧), + 20 台设备整体开销约 0.2 核 CPU + 100KB/s,普通电脑无压力。 + """ + resp = make_response(render_template("admin/wall.html")) + resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" + return resp + + @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": @@ -1020,6 +1033,36 @@ def _screen_get_device(serial): return u2.connect(serial) + + +@app.route("/api/screen/thumb") +@perm_required(PERM_DEVICES) +def api_screen_thumb(): + """大屏缩略图:单张 JPEG(360px 宽,质量 55),供监控大屏轮询。 + + 一次性请求(非流),与任务并发安全(与任务截图同走 u2 minicap)。 + 大屏按需轮询:每设备约 2.5s 一帧,20 台 ≈ 0.2 核 CPU + 100KB/s。 + """ + serial = request.args.get("serial", "") + if not serial: + return jsonify({"ok": False, "error": "缺少 serial"}), 400 + import io + import uiautomator2 as u2 + try: + d = u2.connect(serial) + img = d.screenshot() + if img is None: + return jsonify({"ok": False, "error": "截图失败"}), 503 + if img.width > 360: + ratio = 360 / img.width + img = img.resize((360, int(img.height * ratio))) + buf = io.BytesIO() + img.convert("RGB").save(buf, "JPEG", quality=55) + return Response(buf.getvalue(), mimetype="image/jpeg", + headers={"Cache-Control": "no-store"}) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 503 + @app.route("/api/screen/tap", methods=["POST"]) @perm_required(PERM_DEVICES) def api_screen_tap():