feat: video monitor page with HLS preview

This commit is contained in:
tian 2026-05-07 14:16:21 +08:00
parent 46eb031b8f
commit a289882a1c
3 changed files with 76 additions and 0 deletions

View File

@ -661,6 +661,7 @@ func (u *UI) Routes() (chi.Router, error) {
r.Post("/resources/sync", u.actionResourceSync)
r.Get("/diagnostics", u.pageDiagnostics)
r.Get("/alarms", u.pageAlarms)
r.Get("/monitor", u.pageMonitor)
r.Get("/recognition", u.pageRecognition)
r.Get("/logs", u.pageLogs)
@ -3863,3 +3864,8 @@ func (u *UI) actionDeviceFaceGalleryReload(w http.ResponseWriter, r *http.Reques
}
u.render(w, r, "config_ui", data)
}
func (u *UI) pageMonitor(w http.ResponseWriter, r *http.Request) {
u.ensureDevicesLoaded()
u.render(w, r, "monitor", PageData{Title: "视频监控", Devices: u.registry.GetDevices()})
}

View File

@ -36,6 +36,7 @@
<a class="nav-subitem" href="/ui/models"><span class="nav-icon nav-subicon">{{icon "assets"}}</span><span>模型管理</span></a>
<a class="nav-subitem" href="/ui/resources"><span class="nav-icon nav-subicon">{{icon "template"}}</span><span>资源管理</span></a>
<a class="nav-subitem" href="/ui/alarms"><span class="nav-icon nav-subicon">{{icon "bell"}}</span><span>告警中心</span></a>
t <a class="nav-subitem" href="/ui/monitor"><span class="nav-icon nav-subicon">{{icon "devices"}}</span><span>视频监控</span></a>
<a class="nav-subitem" href="/ui/diagnostics"><span class="nav-icon nav-subicon">{{icon "logs"}}</span><span>日志审计</span></a>
<a class="nav-subitem" href="/ui/system"><span class="nav-icon nav-subicon">{{icon "heartbeat"}}</span><span>系统状态</span></a>
</div>

View File

@ -0,0 +1,69 @@
{{define "monitor"}}
<div class="card">
<div class="section-title">
<div>
<h2 class="title-with-icon">{{icon "devices"}}<span>视频监控</span></h2>
<div class="form-hint">从设备拉取通道列表,点击通道查看实时画面。</div>
</div>
<div class="actions compact">
<button class="btn ghost" type="button" id="btn-load">加载通道</button>
</div>
</div>
</div>
<div class="detail-grid" id="channels-grid" style="grid-template-columns:repeat(2,minmax(0,1fr))">
<div class="card muted" style="grid-column:1/-1;text-align:center">请先选择在线设备,点击"加载通道"。</div>
</div>
<script>
(function() {
var grid = document.getElementById("channels-grid");
function loadChannels(deviceId, deviceIP, agentPort) {
var url = "http://" + deviceIP + ":" + agentPort + "/v1/preview/channels";
grid.innerHTML = '<div class="card muted" style="grid-column:1/-1;text-align:center">加载通道中…</div>';
fetch("http://" + deviceIP + ":" + agentPort + "/v1/preview/channels")
.then(function(r) { return r.json(); })
.then(function(data) {
var chs = data.channels || [];
if (chs.length === 0) {
grid.innerHTML = '<div class="card muted" style="grid-column:1/-1;text-align:center">该设备没有可预览的通道。</div>';
return;
}
var cols = chs.length === 1 ? 1 : 2;
grid.style.gridTemplateColumns = "repeat(" + cols + ",minmax(0,1fr))";
var html = "";
chs.forEach(function(ch) {
html += '<div class="card"><h3>' + esc(ch.name) + '</h3>';
if (ch.hls_url) {
html += '<video controls autoplay muted style="width:100%;max-height:400px;background:#000" src="' + esc(ch.hls_url) + '"></video>';
html += '<div class="actions" style="margin-top:8px"><a class="btn ghost" href="' + esc(ch.hls_url) + '" target="_blank">新窗口打开</a></div>';
} else {
html += '<div class="muted">无预览地址</div>';
}
html += '</div>';
});
grid.innerHTML = html;
})
.catch(function() {
grid.innerHTML = '<div class="card muted" style="grid-column:1/-1;text-align:center">无法连接设备,请确认设备在线。</div>';
});
}
function esc(s) { return (s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"); }
document.getElementById("btn-load").addEventListener("click", function() {
grid.innerHTML = '<div class="card muted" style="grid-column:1/-1;text-align:center">请从列表中选择设备…</div>';
// Create device selector overlay
var html = '<table><thead><tr><th>设备</th><th>地址</th><th>操作</th></tr></thead><tbody>';
{{range .Devices}}
{{if .Online}}
html += '<tr><td>{{.DisplayName}}</td><td class="mono">{{.IP}}:{{.AgentPort}}</td><td><button class="btn ghost" onclick="loadChannels(\'{{.DeviceID}}\',\'{{.IP}}\',{{.AgentPort}})">加载</button></td></tr>';
{{end}}
{{end}}
html += '</tbody></table>';
grid.innerHTML = '<div class="card" style="grid-column:1/-1">' + html + '</div>';
});
})();
</script>
{{end}}