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():