From 14f722fa695e4ac5c0d92675555f3dc202b339bc Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 4 Oct 2026 04:58:56 +0800 Subject: [PATCH] Polish camera settings layout and close media error responses --- README.md | 5 +++++ VERSION | 2 +- app.py | 15 ++++++++------- test_app.py | 2 ++ web/camera-settings.js | 4 ++-- web/style.css | 11 +++++++++++ 6 files changed, 29 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ad5384b..32daae7 100644 --- a/README.md +++ b/README.md @@ -162,3 +162,8 @@ JSON 导出升级为格式 version 2,包含人物、关系和当前账号有 ### 并发与数据一致性 设备对象的校验与保存串行执行;镜头数量限制不能被同时提交绕过。摄像头对象移动空间时,所有镜头归属在同一数据库事务更新。录像机地址或账号更新后,关联通道读取最新端点。无效对象编号、错误 Origin 和非法地址类型会返回明确错误。 + + +### 实机验收完善(v0.1.20) + +摄像机信息及参数改为紧凑布局;全屏监控隐藏管理参数入口。读取或保存时暂时锁定参数输入,避免正在编辑的值被返回结果覆盖。视频服务错误响应也会正确关闭,修复新 Python 版本提示的资源未释放问题。 diff --git a/VERSION b/VERSION index d8a023e..baa9837 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.19 +0.1.20 diff --git a/app.py b/app.py index c066e20..63279f8 100644 --- a/app.py +++ b/app.py @@ -343,13 +343,14 @@ def media_json(path, playback=False): with urllib.request.urlopen(req, timeout=5) as r: return json.load(r) except urllib.error.HTTPError as exc: - if playback and path.startswith('/list?') and exc.code == 404: - try: - detail = json.loads(exc.read(4096)) - if detail.get('error') == 'no recording segments found': - return [] - except (ValueError, AttributeError): - pass + with exc: + if playback and path.startswith('/list?') and exc.code == 404: + try: + detail = json.loads(exc.read(4096)) + if detail.get('error') == 'no recording segments found': + return [] + except (ValueError, AttributeError): + pass raise Problem('视频服务暂不可用,或该时段尚无可回放录像', 503) except Exception: raise Problem('视频服务暂不可用,或该时段尚无可回放录像', 503) diff --git a/test_app.py b/test_app.py index a8796ee..34a2ee1 100644 --- a/test_app.py +++ b/test_app.py @@ -338,12 +338,14 @@ class WebTests(unittest.TestCase): io.BytesIO(b'{"error":"no recording segments found"}')) with patch('app.urllib.request.urlopen', side_effect=error): self.assertEqual(app.media_json('/list?path=example', playback=True), []) + self.assertTrue(error.fp.closed) def test_real_media_service_error_is_preserved(self): error = app.urllib.error.HTTPError('http://localhost/list', 500, 'Error', {}, io.BytesIO(b'{}')) with patch('app.urllib.request.urlopen', side_effect=error): with self.assertRaises(app.Problem): app.media_json('/list?path=example', playback=True) + self.assertTrue(error.fp.closed) def family_save(self, kind, data): return app.FAMILY.save(kind, dict(data, revision=app.setting('familyRevision', 0)), self.admin) diff --git a/web/camera-settings.js b/web/camera-settings.js index 80b848d..24cbd71 100644 --- a/web/camera-settings.js +++ b/web/camera-settings.js @@ -19,7 +19,7 @@ async function loadCameraSettings(){ if(!cameraSettingsContext)return;const context=cameraSettingsContext,generation=++cameraSettingsGeneration; context.settings=null;context.quality=$('#camera-settings-quality').value; $('#camera-settings-status').textContent='正在读取设备实际参数…'; - $$('#camera-settings-dialog form button[type=submit]').forEach(b=>b.disabled=true);$('#camera-settings-reload').disabled=true; + $$('#camera-settings-dialog form input,#camera-settings-dialog form select,#camera-settings-dialog form button[type=submit]').forEach(b=>b.disabled=true);$('#camera-settings-reload').disabled=true; try{const result=await api('/api/camera/settings?'+new URLSearchParams({camera:context.id,quality:context.quality})); if(generation!==cameraSettingsGeneration||context!==cameraSettingsContext||!loggedIn)return;context.settings=result;renderCameraSettings(result); }catch(e){if(!e.stale&&generation===cameraSettingsGeneration)$('#camera-settings-status').textContent=e.message;} @@ -42,7 +42,7 @@ async function saveCameraSettings(form,section){ const context=cameraSettingsContext;if(!context?.settings)return; const values=changedCameraValues(form,section,context.settings);if(!Object.keys(values).length){toast('参数没有变化');return;} const generation=++cameraSettingsGeneration; - $$('#camera-settings-dialog form button[type=submit]').forEach(b=>b.disabled=true);$('#camera-settings-reload').disabled=true;$('#camera-settings-quality').disabled=true; + $$('#camera-settings-dialog form input,#camera-settings-dialog form select,#camera-settings-dialog form button[type=submit]').forEach(b=>b.disabled=true);$('#camera-settings-reload').disabled=true;$('#camera-settings-quality').disabled=true; $('#camera-settings-status').textContent='正在保存并读取设备返回值…'; try{const result=await api('/api/camera/settings',{camera:context.id,quality:context.quality,revision:context.settings.revision,section,values}); if(generation!==cameraSettingsGeneration||context!==cameraSettingsContext||!loggedIn)return; diff --git a/web/style.css b/web/style.css index 14c8f23..a8c07e3 100644 --- a/web/style.css +++ b/web/style.css @@ -11,3 +11,14 @@ textarea{font:inherit;color:#e3f0ef;background:#111e24;border:1px solid #3a4e55; .brand-icon{width:44px;height:44px;object-fit:cover;border-radius:12px;flex-shrink:0}.wall-toolbar{display:flex;align-items:center;gap:15px;flex-wrap:wrap;margin:16px 0}.wall-toolbar select{padding:8px}.wall-paging{display:flex;align-items:center;gap:9px;color:var(--muted);font-size:12px}#wall-clock{margin-left:auto;color:var(--muted);font-size:12px}.live-grid[data-layout="1"]{grid-template-columns:1fr}.live-grid[data-layout="2"],.live-grid[data-layout="4"]{grid-template-columns:repeat(2,minmax(0,1fr))}.live-grid[data-layout="6"],.live-grid[data-layout="9"]{grid-template-columns:repeat(3,minmax(0,1fr))}.live-grid[data-layout="16"]{grid-template-columns:repeat(4,minmax(0,1fr))}.wall-reorder{display:none;padding:8px 12px;align-items:center;gap:8px}.arranging .wall-reorder{display:flex}.wall-reorder span{cursor:grab;margin-right:auto;font-size:12px;color:var(--muted)}.wall-reorder button{padding:4px 9px;font-size:11px}#page-live:fullscreen{background:#070f12;padding:12px;width:100vw;height:100dvh;display:flex;flex-direction:column;overflow:hidden}#page-live:fullscreen>.section-head,#page-live:fullscreen>#quality-note{display:none}#page-live:fullscreen .wall-toolbar{margin:0 0 10px;flex-shrink:0}#page-live:fullscreen .live-grid{flex:1;min-height:0;gap:8px;grid-template-rows:repeat(2,minmax(0,1fr))}#page-live:fullscreen .live-grid[data-layout="1"],#page-live:fullscreen .live-grid[data-layout="2"]{grid-template-rows:1fr}#page-live:fullscreen .live-grid[data-layout="9"]{grid-template-rows:repeat(3,minmax(0,1fr))}#page-live:fullscreen .live-grid[data-layout="16"]{grid-template-rows:repeat(4,minmax(0,1fr))}#page-live:fullscreen .camera-card{display:flex;flex-direction:column;min-height:0;position:relative}#page-live:fullscreen .video-panel{flex:1;min-height:0;aspect-ratio:auto}#page-live:fullscreen .camera-meta{padding:7px 10px;align-items:center}#page-live:fullscreen .camera-meta p,#page-live:fullscreen .camera-meta .badge{display:none}#page-live:fullscreen .camera-meta strong{font-size:12px}#page-live:fullscreen .camera-actions{position:absolute;bottom:34px;right:6px;padding:4px;background:#10191ddd;border-radius:6px;opacity:0}#page-live:fullscreen .camera-card:hover .camera-actions,#page-live:fullscreen .camera-card:focus-within .camera-actions{opacity:1}#page-live:fullscreen .camera-actions [data-edit-camera],#page-live:fullscreen .camera-actions [data-play-camera]{display:none}.ptz-panel{background:#1a3038;border:1px solid #41656c;border-radius:12px;padding:16px;display:flex;align-items:center;gap:22px;position:fixed;bottom:22px;right:22px;z-index:4;max-width:calc(100vw - 32px);box-shadow:0 12px 50px #0008}.ptz-panel p{font-size:12px;color:var(--muted);max-width:320px;margin:8px 0 0}.ptz-pad{display:grid;grid-template-columns:repeat(3,46px);grid-template-rows:repeat(3,40px);gap:4px}.ptz-pad button{padding:5px;font-size:17px}.ptz-pad [data-ptz-action="up"]{grid-column:2}.ptz-pad [data-ptz-action="left"]{grid-column:1;grid-row:2}.ptz-pad [data-ptz-action="stop"]{grid-column:2;grid-row:2;background:#f0bda6;font-size:12px}.ptz-pad [data-ptz-action="right"]{grid-column:3;grid-row:2}.ptz-pad [data-ptz-action="down"]{grid-column:2;grid-row:3}.family-date{border:1px solid var(--line);border-radius:8px;padding:14px;margin:0;min-width:0}.family-date legend{font-size:13px;color:#a5bec1}.family-date label{margin-bottom:10px}.family-date output{display:block;font-size:12px;line-height:1.7;color:var(--accent)}@media(max-width:760px){.wall-toolbar{gap:9px}#wall-clock{display:none}.camera-meta strong{font-size:12px}.camera-actions{flex-wrap:wrap;padding:0 8px 8px}.camera-meta{padding:9px}.ptz-panel{flex-wrap:wrap;gap:12px}.ptz-panel>div:first-child{flex:1;min-width:160px}.family-date{grid-column:1/-1}#page-live:fullscreen .camera-actions{opacity:1}.live-grid[data-layout="16"] .camera-meta strong{font-size:9px}} #page-live:fullscreen .live-grid[data-layout="auto"]{grid-template-columns:repeat(3,minmax(0,1fr))} .wall-reorder [data-wall-drag]{touch-action:none;user-select:none;padding:5px 0}.camera-card.wall-drop-target{outline:2px solid var(--accent);outline-offset:-3px} + +#camera-settings-dialog{width:min(1060px,94vw)} +#camera-settings-info dl{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin:18px 0 24px;padding:16px;border:1px solid var(--line);border-radius:10px} +#camera-settings-info dt{font-size:12px;color:var(--muted);margin-bottom:6px} +#camera-settings-info dd{font-size:13px;margin:0;overflow-wrap:anywhere} +#camera-image-fields,#camera-video-fields{grid-template-columns:repeat(3,minmax(0,1fr))} +#camera-settings-status:empty{display:none} +#camera-settings-dialog details{margin-top:20px;color:var(--muted);font-size:13px;line-height:1.8} +#page-live:fullscreen .camera-actions [data-camera-settings]{display:none} +@media(max-width:700px){#camera-settings-info dl,#camera-image-fields,#camera-video-fields{grid-template-columns:1fr 1fr}} +@media(max-width:420px){#camera-settings-info dl,#camera-image-fields,#camera-video-fields{grid-template-columns:1fr}}