safesight-control/internal/web/ui/templates/console.html

276 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{define "console"}}
<div class="stats">
<div class="stat">设备 <strong>{{len .ConsoleDevices}}</strong></div>
<div class="stat">视频源 <strong>{{len .ConsoleAllVideoSources}}</strong></div>
<div class="stat">未分配 <strong>{{len .ConsoleVideoSources}}</strong></div>
</div>
<div class="card-tabs console-tabs-wrap" style="margin-top:16px">
<ul class="nav nav-tabs" role="tablist" style="display:flex;align-items:center">
<li class="nav-item" role="presentation">
<a class="nav-link active console-tab" data-panel="device" role="tab" aria-selected="true">📋 设备视图</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link console-tab" data-panel="video" role="tab" aria-selected="false">🎥 视频视图</a>
</li>
<li style="margin-left:auto;display:flex;gap:10px;align-items:center;padding:0 12px">
<span class="muted small">⬤ 上次下发成功</span>
<form method="post" action="/ui/console" style="display:inline">
<button class="btn" type="submit" style="padding:4px 14px;font-size:12px">保存并下发</button>
</form>
</li>
</ul>
<div class="tab-content" style="padding:0">
<div id="console-panel-device" class="card tab-pane active show" style="padding:16px;margin:0;border-top-left-radius:0">
{{range $i, $cd := .ConsoleDevices}}
<div class="console-device-row">
<div class="console-header">
<span class="status-dot {{if $cd.Device.Online}}ok{{else}}bad{{end}}"></span>
<span class="console-device-name">{{$cd.Device.DisplayName}}</span>
<span class="console-device-meta">{{$cd.Device.IP}} | {{len $cd.Channels}}/{{$cd.MaxUnits}} 路</span>
<span class="console-cam-add" style="margin-top:0;padding-top:0;font-size:11px"> 从未分配添加</span>
</div>
<div class="console-body">
<!-- LEFT: Preview tiles in one row -->
<div class="console-preview">
{{if $cd.Channels}}
{{range $ch := $cd.Channels}}
<div class="preview-tile" data-device-id="{{$cd.Device.DeviceID}}" data-channel="{{$ch.Name}}">
<video class="console-video" muted playsinline preload="none"></video>
<span class="preview-label">{{$ch.Display}}</span>
</div>
{{end}}
{{else}}
<div class="preview-tile empty">
</div>
{{end}}
{{$remaining := sub $cd.MaxUnits (len $cd.Channels)}}
{{if gt $remaining 0}}
{{range $j := loop $remaining}}
<div class="preview-tile empty"></div>
{{end}}
{{end}}
</div>
<!-- RIGHT: Features and Cameras side by side -->
<div class="console-controls">
<div class="console-section">
<div class="console-section-title">检测功能</div>
<table class="console-feature-table">
{{range $f := $cd.Features}}
<tr>
<td class="console-feature-check"><input type="checkbox" name="feature_{{$cd.Device.DeviceID}}" value="{{$f.Key}}"{{if $f.Enabled}} checked{{end}}></td>
<td class="console-feature-label">{{$f.Label}}</td>
</tr>
{{end}}
</table>
</div>
<div class="console-section">
<div class="console-section-title">设备负载</div>
<div class="console-metrics">
<div class="metric-row" style="font-size:9px"><span>CPU</span><span>{{printf "%.0f%%" $cd.CPUUsage}}</span></div>
<div class="console-metric-bar"><div style="height:100%;border-radius:2px;width:{{printf "%.0f%%" $cd.CPUUsage}};background:{{if gt $cd.CPUUsage 80.0}}var(--bar-red){{else if gt $cd.CPUUsage 50.0}}var(--bar-amber){{else}}var(--bar-green){{end}}"></div></div>
<div class="metric-row" style="font-size:9px"><span>内存</span><span>{{printf "%.0f%%" $cd.MemUsage}}</span></div>
<div class="console-metric-bar"><div style="height:100%;border-radius:2px;width:{{printf "%.0f%%" $cd.MemUsage}};background:{{if gt $cd.MemUsage 80.0}}var(--bar-red){{else if gt $cd.MemUsage 50.0}}var(--bar-amber){{else}}var(--bar-green){{end}}"></div></div>
<div class="metric-row" style="font-size:9px"><span>NPU</span><span>{{printf "%.0f%%" $cd.NPUUsage}}</span></div>
<div class="console-metric-bar"><div style="height:100%;border-radius:2px;width:{{printf "%.0f%%" $cd.NPUUsage}};background:{{if gt $cd.NPUUsage 80.0}}var(--bar-red){{else if gt $cd.NPUUsage 50.0}}var(--bar-amber){{else}}var(--bar-green){{end}}"></div></div>
<div class="metric-row" style="font-size:9px"><span>磁盘</span><span>{{printf "%.0f%%" $cd.DiskUsage}}</span></div>
<div class="console-metric-bar"><div style="height:100%;border-radius:2px;width:{{printf "%.0f%%" $cd.DiskUsage}};background:{{if gt $cd.DiskUsage 80.0}}var(--bar-red){{else if gt $cd.DiskUsage 50.0}}var(--bar-amber){{else}}var(--bar-green){{end}}"></div></div>
<div class="metric-row" style="font-size:9px"><span>温度</span><span>{{printf "%.0f°C" $cd.Temperature}}</span></div>
<div class="console-metric-bar"><div style="height:100%;border-radius:2px;width:{{printf "%.0f%%" $cd.Temperature}};background:{{if gt $cd.Temperature 60.0}}var(--bar-red){{else if gt $cd.Temperature 40.0}}var(--bar-amber){{else}}var(--bar-green){{end}}"></div></div>
</div>
</div>
</div>
</div>
</div>
{{else}}
<div class="empty-state" style="padding:0;border:none;background:transparent">
<div class="empty-title">暂无可管理设备</div>
<div class="muted">请先在"设备"页添加或发现设备。</div>
</div>
{{end}}
<!-- Unassigned pool -->
<div style="margin-top:16px">
<div class="section-title">
<div>
<h3 class="title-with-icon">{{icon "device"}}<span>未分配视频源({{len .ConsoleVideoSources}}路)</span></h3>
</div>
</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:8px">
{{range $s := .ConsoleVideoSources}}
<div class="console-source-card">
<div class="console-source-name">{{$s.Name}}</div>
<div class="console-source-meta">{{if $s.Area}}{{$s.Area}} · {{end}}{{$s.SourceSummary}}</div>
<div class="console-source-assign"> 分配到设备</div>
</div>
{{else}}
<span class="muted small">暂无未分配视频源</span>
{{end}}
</div>
</div>
</div><!-- end device panel -->
<div id="console-panel-video" class="card tab-pane" style="padding:16px;margin:0;border-top-left-radius:0">
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px">
{{range $cd := $.ConsoleDevices}}
{{range $ch := $cd.Channels}}
<div class="video-card">
<div class="video-card-preview" data-device-id="{{$cd.Device.DeviceID}}" data-channel="{{$ch.Name}}">
<video class="video-card-video" muted playsinline preload="none"></video>
</div>
<div class="video-card-info">
<div class="video-card-name">{{$ch.SourceName}}</div>
<div class="video-card-device">{{$cd.Device.DisplayName}}</div>
</div>
</div>
{{end}}
{{end}}
{{range $s := $.ConsoleVideoSources}}
<div class="video-card" style="border:1px dashed #555">
<div style="background:#000;aspect-ratio:16/9;display:flex;align-items:center;justify-content:center;overflow:hidden">
<span style="color:#666;font-size:12px">⏹ {{$s.Name}}</span>
</div>
<div style="padding:8px 10px">
<div style="font-size:13px;font-weight:500;margin-bottom:2px">{{$s.Name}}</div>
<a href="#" style="font-size:11px;color:var(--primary);text-decoration:none"> 分配到设备</a>
</div>
</div>
{{end}}
</div>
</div>
</div><!-- tab-content -->
</div><!-- card-tabs -->
<script src="/ui/assets/vendor/hls.min.js"></script>
<script>
(function() {
// Tab switching
document.querySelectorAll('.console-tab').forEach(function(tab) {
tab.addEventListener('click', function(e) {
e.preventDefault();
document.querySelectorAll('.console-tab').forEach(function(t) {
t.classList.remove('active');
t.setAttribute('aria-selected', 'false');
});
document.querySelectorAll('.tab-pane').forEach(function(p) {
p.classList.remove('active', 'show');
});
tab.classList.add('active');
tab.setAttribute('aria-selected', 'true');
var panel = document.getElementById('console-panel-' + tab.getAttribute('data-panel'));
if (panel) {
panel.classList.add('active', 'show');
// Initialize video view players on first switch
if (tab.getAttribute('data-panel') === 'video' && !tab._videoInit) {
tab._videoInit = true;
initVideoPlayers('.video-card-preview[data-device-id]');
}
}
});
});
function initVideoPlayers(selector) {
var tiles = document.querySelectorAll(selector);
var deviceChannels = {};
tiles.forEach(function(tile) {
var did = tile.getAttribute('data-device-id');
var ch = tile.getAttribute('data-channel');
if (!did || !ch) return;
if (!deviceChannels[did]) deviceChannels[did] = {};
deviceChannels[did][ch] = tile.querySelector('video');
});
var deviceIds = Object.keys(deviceChannels);
deviceIds.forEach(function(did) {
fetch('/ui/api/monitor/channels?device_id=' + encodeURIComponent(did))
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.channels) return;
data.channels.forEach(function(ch) {
var video = deviceChannels[did][ch.name];
if (!video) return;
var proxyUrl = '/ui/hls/' + encodeURIComponent(did) + '/hls/' + encodeURIComponent(ch.name) + '/index.m3u8';
if (Hls.isSupported()) {
var hls = new Hls();
hls.loadSource(proxyUrl);
hls.attachMedia(video);
video.play().catch(function(){});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = proxyUrl;
video.play().catch(function(){});
}
});
})
.catch(function() {});
});
}
// Init device view players immediately
initVideoPlayers('.preview-tile[data-device-id]');
})();
</script>
<style>
.console-section-title { font-size: 10px; color: var(--muted); margin-bottom: 8px; text-transform: uppercase; letter-spacing: .8px; font-weight: 600; }
.console-section { background: var(--surface-soft); border-radius: 6px; padding: 10px 12px; width: 50%; overflow: hidden; }
.console-device-row { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--border); border-radius: 8px; padding: 14px; margin-bottom: 12px; }
.console-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.console-device-name { font-weight: 600; font-size: 14px; }
.console-device-meta { font-size: 11px; color: var(--muted); }
.console-body { display: flex; gap: 14px; min-height: 80px; }
/* Preview: single row, 4 equal tiles */
.console-preview { flex: 1; min-width: 350px; background: #0d0d0d; border-radius: 6px; padding: 4px; display: flex; gap: 4px; }
.preview-tile { background: #000; border-radius: 4px; width: 25%; aspect-ratio: 16/9; position: relative; overflow: hidden; }
.preview-tile.empty { background: #080808; }
.preview-tile video { width: 100%; height: 100%; object-fit: cover; display: block; }
.preview-label { position: absolute; bottom: 3px; left: 4px; background: rgba(0,0,0,0.7); color: #aaa; padding: 1px 5px; border-radius: 3px; font-size: 9px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: calc(100% - 8px); pointer-events: none; z-index: 1; }
/* Controls: side by side, 50/50 */
.console-controls { width: 460px; flex-shrink: 0; display: flex; gap: 10px; }
.console-section { background: var(--surface-soft); border-radius: 6px; padding: 10px 12px; width: 50%; overflow: hidden; }
.console-feature-table { width: 100%; border-collapse: collapse; border: none; background: transparent; }
.console-feature-table tr { border: none; }
.console-feature-table td { border: none; padding: 2px 0; vertical-align: middle; background: transparent; }
.console-feature-check { width: 18px; }
.console-feature-check input { margin: 0; display: block; }
.console-feature-label { font-size: 12px; white-space: nowrap; }
.console-cam-add { margin-top: auto; padding-top: 6px; font-size: 11px; color: var(--primary); cursor: pointer; display: inline-flex; align-items: center; gap: 3px; }
/* Metrics in console */
.console-metrics { display: flex; flex-direction: column; gap: 2px; }
.metric-row { display: flex; justify-content: space-between; font-size: 11px; padding: 1px 0; }
.console-controls { --bar-green: #66c98f; --bar-amber: #d8a657; --bar-red: #e46f72; }
.console-metric-bar { height: 3px; background: #c0c0c0; margin-bottom: 1px; border-radius: 2px; overflow: hidden; }
/* Console tabs flush */
.card-tabs.console-tabs-wrap .nav-tabs { margin-bottom: 0; }
.card-tabs.console-tabs-wrap .tab-content { padding: 0; }
.card-tabs.console-tabs-wrap .tab-content > .card { margin-top: -1px; border-top-left-radius: 0; border-top-right-radius: 0; }
.card-tabs.console-tabs-wrap .nav-tabs .nav-link { padding: 8px 20px; font-size: 13px; }
.console-source-card { background: var(--surface-soft); border: 1px dashed var(--border); border-radius: 8px; padding: 12px; width: 180px; }
.console-source-name { font-size: 13px; font-weight: 500; margin-bottom: 4px; }
.console-source-meta { font-size: 11px; color: var(--muted); }
.console-source-assign { margin-top: 6px; font-size: 11px; color: var(--primary); cursor: pointer; }
.video-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.video-card-preview { background: #000; aspect-ratio: 16/9; display: flex; align-items: center; justify-content: center; overflow: hidden; position: relative; }
.video-card-preview video { width: 100%; height: 100%; object-fit: cover; display: block; }
.video-card-info { padding: 8px 10px; }
.video-card-name { font-size: 13px; font-weight: 500; }
.video-card-device { font-size: 11px; color: var(--primary); margin-top: 2px; }
.video-card-device.muted { color: var(--muted); }
</style>
{{end}}