506 lines
26 KiB
HTML
506 lines
26 KiB
HTML
{{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 id="console-status" class="muted small"></span>
|
||
<button id="console-save-btn" class="btn" type="button" disabled style="padding:4px 14px;font-size:12px;opacity:0.5">保存并下发</button>
|
||
</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">
|
||
<form id="console-form" method="post" action="/console" style="display:contents">
|
||
|
||
{{range $i, $cd := .ConsoleDevices}}
|
||
<div class="console-device-row" data-device-id="{{$cd.Device.DeviceID}}">
|
||
<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;cursor:pointer" onclick="showUnassignedForDevice('{{$cd.Device.DeviceID}}')">+ 从未分配添加</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="device_{{$cd.Device.DeviceID}}_feature" value="{{$f.Key}}"{{if $f.Enabled}} checked{{end}}{{if not $f.Available}} disabled{{end}}></td>
|
||
<td class="console-feature-label{{if $f.Conflict}} text-danger{{else if not $f.Available}} text-muted{{end}}" title="{{$f.Description}}{{if $f.Conflict}} (设备已不支持){{else if not $f.Available}} (设备不支持){{end}}">
|
||
{{$f.Label}}{{if $f.Conflict}} ⚠{{end}}
|
||
</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>
|
||
<!-- hidden: assigned video sources for this device -->
|
||
{{range $ch := $cd.Channels}}
|
||
<input type="hidden" name="device_{{$cd.Device.DeviceID}}_source" value="{{$ch.SourceName}}">
|
||
{{end}}
|
||
</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" data-source="{{$s.Name}}" onclick="assignSourceToDevice('{{$s.Name}}')">+ 分配到设备</div>
|
||
</div>
|
||
{{else}}
|
||
<span class="muted small">暂无未分配视频源</span>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
</form>
|
||
</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}} · {{$cd.Device.IP}}</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" onclick="event.preventDefault();showDevicePicker('{{$s.Name}}');">+ 分配到设备</a>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- tab-content -->
|
||
</div><!-- card-tabs -->
|
||
|
||
<script src="/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('/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 = '/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]');
|
||
|
||
// Source assignment
|
||
window.assignSourceToDevice = function(sourceName) {
|
||
// Check if a specific device context exists
|
||
var ctx = this._assignContext;
|
||
if (ctx && ctx.deviceID) {
|
||
addSourceToDeviceForm(ctx.deviceID, sourceName);
|
||
document.querySelector('.source-assign-overlay').remove();
|
||
return;
|
||
}
|
||
showDevicePicker(sourceName);
|
||
};
|
||
|
||
window.showUnassignedForDevice = function(deviceID) {
|
||
var cards = document.querySelectorAll('.console-source-card');
|
||
var visible = [];
|
||
cards.forEach(function(card) {
|
||
if (card.style.display !== 'none') {
|
||
var nameEl = card.querySelector('.console-source-name');
|
||
if (nameEl) visible.push(nameEl.textContent.trim());
|
||
}
|
||
});
|
||
if (visible.length === 0) { alert('暂无未分配视频源'); return; }
|
||
var overlay = document.createElement('div');
|
||
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center';
|
||
overlay.addEventListener('click',function(){overlay.remove()});
|
||
var box = document.createElement('div');
|
||
box.style.cssText = 'background:var(--surface);border-radius:8px;padding:20px;min-width:300px;max-width:400px';
|
||
box.addEventListener('click',function(e){e.stopPropagation()});
|
||
box.innerHTML = '<div style="font-size:14px;font-weight:600;margin-bottom:12px">为设备添加视频源 (' + visible.length + '路可用)</div>' +
|
||
'<div style="max-height:60vh;overflow-y:auto;margin-bottom:12px">';
|
||
visible.forEach(function(name) {
|
||
var item = document.createElement('div');
|
||
item.textContent = name;
|
||
item.style.cssText = 'padding:8px 12px;cursor:pointer;border-radius:4px;margin-bottom:2px;font-size:13px';
|
||
item.addEventListener('mouseover',function(){item.style.background='var(--surface-soft)'});
|
||
item.addEventListener('mouseout',function(){item.style.background='transparent'});
|
||
item.addEventListener('click',function(){
|
||
addSourceToDeviceForm(deviceID, name);
|
||
overlay.remove();
|
||
});
|
||
box.querySelector('div:last-child').appendChild(item);
|
||
});
|
||
box.innerHTML += '</div><div style="text-align:right"><button class="btn" style="font-size:12px">取消</button></div>';
|
||
box.querySelector('button').addEventListener('click',function(){overlay.remove()});
|
||
overlay.appendChild(box);
|
||
document.body.appendChild(overlay);
|
||
};
|
||
|
||
function addSourceToDeviceForm(deviceID, sourceName) {
|
||
var row = document.querySelector('.console-device-row[data-device-id="' + deviceID + '"]');
|
||
if (!row) return;
|
||
var existing = row.querySelector('input[name="device_' + deviceID + '_source"][value="' + sourceName + '"]');
|
||
if (existing) return;
|
||
var input = document.createElement('input');
|
||
input.type = 'hidden';
|
||
input.name = 'device_' + deviceID + '_source';
|
||
input.value = sourceName;
|
||
row.appendChild(input);
|
||
// Hide source card
|
||
var card = document.querySelector('.console-source-assign[data-source="' + sourceName + '"]');
|
||
if (card) {
|
||
var sc = card.closest('.console-source-card');
|
||
if (sc) sc.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function showDevicePicker(sourceName) {
|
||
var rows = document.querySelectorAll('.console-device-row');
|
||
if (rows.length === 0) { alert('暂无可分配设备'); return; }
|
||
var overlay = document.createElement('div');
|
||
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center';
|
||
overlay.addEventListener('click',function(){overlay.remove()});
|
||
var box = document.createElement('div');
|
||
box.style.cssText = 'background:var(--surface);border-radius:8px;padding:20px;min-width:300px;max-width:400px';
|
||
box.addEventListener('click',function(e){e.stopPropagation()});
|
||
box.innerHTML = '<div style="font-size:14px;font-weight:600;margin-bottom:12px">分配 "' + sourceName + '" 到设备</div>' +
|
||
'<div style="max-height:60vh;overflow-y:auto;margin-bottom:12px">';
|
||
rows.forEach(function(row) {
|
||
var did = row.getAttribute('data-device-id');
|
||
var nameEl = row.querySelector('.console-device-name');
|
||
var dname = nameEl ? nameEl.textContent : did;
|
||
var metaEl = row.querySelector('.console-device-meta');
|
||
var ip = metaEl ? (metaEl.textContent.match(/[\d.]+/) || [''])[0] : '';
|
||
var item = document.createElement('div');
|
||
item.style.cssText = 'padding:8px 12px;cursor:pointer;border-radius:4px;margin-bottom:2px;font-size:13px';
|
||
item.innerHTML = dname + ' <span style="font-size:11px;color:var(--muted)">' + (ip || did) + '</span>';
|
||
item.addEventListener('mouseover',function(){item.style.background='var(--surface-soft)'});
|
||
item.addEventListener('mouseout',function(){item.style.background='transparent'});
|
||
item.addEventListener('click',function(){
|
||
addSourceToDeviceForm(did, sourceName);
|
||
overlay.remove();
|
||
});
|
||
box.querySelector('div:last-child').appendChild(item);
|
||
});
|
||
box.innerHTML += '</div><div style="text-align:right"><button class="btn" style="font-size:12px">取消</button></div>';
|
||
box.querySelector('button').addEventListener('click',function(){overlay.remove()});
|
||
overlay.appendChild(box);
|
||
document.body.appendChild(overlay);
|
||
}
|
||
|
||
// ---- dirty state tracking ----
|
||
var initialFeatures = {};
|
||
var initialSources = {};
|
||
document.querySelectorAll('.console-device-row').forEach(function(row) {
|
||
var did = row.getAttribute('data-device-id');
|
||
initialFeatures[did] = [];
|
||
initialSources[did] = [];
|
||
row.querySelectorAll('input[type=checkbox]').forEach(function(cb) {
|
||
if (cb.checked) initialFeatures[did].push(cb.value);
|
||
});
|
||
row.querySelectorAll('input[type=hidden][name^=device_]').forEach(function(h) {
|
||
initialSources[did].push(h.value);
|
||
});
|
||
});
|
||
|
||
function isDirty() {
|
||
var rows = document.querySelectorAll('.console-device-row');
|
||
for (var i = 0; i < rows.length; i++) {
|
||
var did = rows[i].getAttribute('data-device-id');
|
||
var feats = [];
|
||
rows[i].querySelectorAll('input[type=checkbox]:checked').forEach(function(cb) { feats.push(cb.value); });
|
||
if (feats.sort().join(',') !== (initialFeatures[did]||[]).sort().join(',')) return true;
|
||
var srcs = [];
|
||
rows[i].querySelectorAll('input[type=hidden][name^=device_]').forEach(function(h) { srcs.push(h.value); });
|
||
if (srcs.sort().join(',') !== (initialSources[did]||[]).sort().join(',')) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function updateSaveButton() {
|
||
var btn = document.getElementById('console-save-btn');
|
||
if (isDirty()) {
|
||
btn.disabled = false;
|
||
btn.style.opacity = '1';
|
||
btn.style.cursor = 'pointer';
|
||
} else {
|
||
btn.disabled = true;
|
||
btn.style.opacity = '0.5';
|
||
btn.style.cursor = 'default';
|
||
}
|
||
}
|
||
|
||
// Listen for changes
|
||
document.querySelectorAll('.console-device-row input[type=checkbox]').forEach(function(cb) {
|
||
cb.addEventListener('change', updateSaveButton);
|
||
});
|
||
|
||
// Override addSourceToDeviceForm to also trigger dirty check
|
||
var _origAddSource = addSourceToDeviceForm;
|
||
addSourceToDeviceForm = function(did, src) {
|
||
_origAddSource(did, src);
|
||
updateSaveButton();
|
||
};
|
||
|
||
// Confirmation dialog (one-shot listener using {once:true})
|
||
var saveBtn = document.getElementById('console-save-btn');
|
||
var confirmHandler = function() {
|
||
if (!isDirty()) return;
|
||
var summary = '';
|
||
var dirtyDevs = [];
|
||
document.querySelectorAll('.console-device-row').forEach(function(row) {
|
||
var did = row.getAttribute('data-device-id');
|
||
var feats = [];
|
||
row.querySelectorAll('input[type=checkbox]:checked').forEach(function(cb) { feats.push(cb.value); });
|
||
if (feats.sort().join(',') === (initialFeatures[did]||[]).sort().join(',')) return;
|
||
dirtyDevs.push(did);
|
||
var nameEl = row.querySelector('.console-device-name');
|
||
var dname = nameEl ? nameEl.textContent : did;
|
||
summary += '<div style="font-weight:600;margin:8px 0 4px">' + dname + '</div>';
|
||
feats.forEach(function(f) {
|
||
summary += '<div style="font-size:12px;padding-left:12px">☑ ' + f + '</div>';
|
||
});
|
||
});
|
||
if (!summary) { alert('未选择任何检测功能或视频源'); return; }
|
||
var hasFeature = false;
|
||
document.querySelectorAll('.console-device-row input[type=checkbox]:checked').forEach(function() { hasFeature = true; });
|
||
if (!hasFeature) { alert('请至少选择一项检测功能'); return; }
|
||
var overlay = document.createElement('div');
|
||
overlay.className = 'confirm-overlay';
|
||
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center';
|
||
overlay.innerHTML = '<div style="background:var(--surface);border-radius:8px;padding:24px;min-width:360px;max-width:500px" onclick="event.stopPropagation()">' +
|
||
'<div style="font-size:16px;font-weight:600;margin-bottom:4px">确认下发配置</div>' +
|
||
'<div style="font-size:12px;color:var(--muted);margin-bottom:16px">以下配置将被渲染并下发到设备,edge-server 将热更新。</div>' +
|
||
summary +
|
||
'<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end">' +
|
||
'<button class="btn secondary" style="font-size:12px" onclick="this.closest(\'.confirm-overlay\').remove()">取消</button>' +
|
||
'<button class="btn" style="font-size:12px" id="confirm-deploy-btn">确认下发</button>' +
|
||
'</div></div>';
|
||
overlay.addEventListener('click', function() { overlay.remove(); });
|
||
document.body.appendChild(overlay);
|
||
document.getElementById('confirm-deploy-btn').addEventListener('click', function() {
|
||
if (dirtyDevs.length === 0) { overlay.remove(); return; }
|
||
var input = document.createElement('input');
|
||
input.type = 'hidden';
|
||
input.name = 'dirty_device_ids';
|
||
input.value = dirtyDevs.join(',');
|
||
document.getElementById('console-form').appendChild(input);
|
||
overlay.remove();
|
||
document.getElementById('console-form').submit();
|
||
}, {once: true});
|
||
};
|
||
saveBtn.addEventListener('click', confirmHandler);
|
||
|
||
// Show status from URL param, then clear the param so refresh doesn't repeat it.
|
||
(function() {
|
||
var m = location.search.match(/[?&]task=([^&]+)/);
|
||
var msg = location.search.match(/[?&]msg=([^&]+)/);
|
||
if (m) {
|
||
document.getElementById('console-status').innerHTML = '⏳ 任务 ' + m[1].substring(0,8) + ' 执行中,' + (msg ? decodeURIComponent(msg[1]) : '页面将自动刷新...');
|
||
setTimeout(function(){ location.href = '/console'; }, 8000);
|
||
} else if (msg) {
|
||
document.getElementById('console-status').innerHTML = decodeURIComponent(msg[1]);
|
||
setTimeout(function(){ document.getElementById('console-status').innerHTML = ''; }, 5000);
|
||
}
|
||
// Clear URL params so refresh doesn't re-show stale messages.
|
||
if (location.search) {
|
||
var url = new URL(location.href);
|
||
url.searchParams.delete('msg');
|
||
url.searchParams.delete('task');
|
||
url.searchParams.delete('error');
|
||
history.replaceState(null, '', url.href);
|
||
}
|
||
})();
|
||
})();
|
||
</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}}
|