3588AdminBackend/internal/web/ui/templates/devices.html

240 lines
7.9 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 "devices"}}
<div class="card">
<h2>发现设备</h2>
<div class="row" style="gap:16px">
<form method="post" action="/ui/discovery/search" class="row">
<div>
<div class="muted small">超时(毫秒)</div>
<input name="timeout_ms" value="1200" />
</div>
<div style="align-self:end">
<button type="submit">搜索UDP 广播)</button>
</div>
</form>
<div style="align-self:end">
<a href="/ui/devices-add"><button type="button">手动添加设备</button></a>
</div>
</div>
</div>
<div class="card">
<h2>概览</h2>
<div class="stats">
<div class="stat"><div class="k">已登记设备</div><div class="v">{{.DeviceCount}}</div></div>
<div class="stat"><div class="k">在线</div><div class="v">{{.OnlineCount}}</div></div>
<div class="stat"><div class="k">离线</div><div class="v">{{.OfflineCount}}</div></div>
<div class="stat"><div class="k">本次发现</div><div class="v">{{.FoundCount}}</div></div>
</div>
</div>
{{if .Found}}
<div class="card">
<h2>本次发现({{len .Found}}</h2>
<div class="muted small">提示发现结果会同步写入下方“设备列表Registry”。</div>
<div class="table-wrap" style="margin-top:10px">
<table>
<thead>
<tr><th>设备ID</th><th>名称</th><th>地址</th><th>Media 端口</th><th>版本</th></tr>
</thead>
<tbody>
{{range .Found}}
<tr>
<td><a class="mono" href="/ui/devices/{{.DeviceID}}">{{.DeviceID}}</a></td>
<td>{{.DeviceName}}</td>
<td class="mono">{{.IP}}:{{.AgentPort}}</td>
<td class="mono">{{.MediaPort}}</td>
<td class="mono">{{.Version}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{end}}
<div class="card">
<h2>设备列表Registry</h2>
<div class="row" style="margin-top:10px">
<div>
<div class="muted small">筛选设备ID / 名称 / IP</div>
<input id="filter" placeholder="输入关键字..." />
</div>
<div class="muted small" style="align-self:end">由 discovery 与轮询自动更新</div>
</div>
<form method="post" action="/ui/devices/batch-action" style="margin-top:10px">
<div class="row">
<div>
<div class="muted small">批量操作</div>
<select name="action">
<option value="media_start">启动 Media</option>
<option value="media_restart">重启 Media</option>
<option value="media_stop">停止 Media</option>
<option value="reload">重载</option>
<option value="rollback">回滚</option>
</select>
</div>
<div>
<div class="muted small">config可选仅 start/restart 生效)</div>
<input name="config" placeholder="cam1" />
</div>
<div style="align-self:end">
<button type="submit" onclick="return confirm('确认对所选设备执行批量操作?')">执行(已选 <span id="selectedCount">0</span></button>
</div>
</div>
<div class="table-wrap" style="margin-top:10px">
<table id="devices-table">
<thead>
<tr>
<th style="width:48px"><input id="selectAll" type="checkbox" title="全选(仅作用于当前筛选可见行)" /></th>
<th>设备ID</th><th>名称</th><th>地址</th><th>状态</th><th>主程序</th><th>最后在线</th><th>版本</th>
</tr>
</thead>
<tbody>
{{range .Devices}}
<tr>
<td><input class="dev-check" type="checkbox" name="device_id" value="{{.DeviceID}}" /></td>
<td><a class="mono" href="/ui/devices/{{.DeviceID}}">{{.DeviceID}}</a></td>
<td>{{.DeviceName}}</td>
<td class="mono">
{{.IP}}:{{.AgentPort}}
<div class="muted small">media: {{.MediaPort}}</div>
</td>
<td>{{if .Online}}<span class="pill ok">在线</span>{{else}}<span class="pill bad">离线</span>{{end}}</td>
<td>
{{if .Online}}
<span class="pill warn" data-media-status="{{.DeviceID}}">待查询</span>
{{else}}
<span class="muted">-</span>
{{end}}
</td>
<td>
<div>{{ago .LastSeenMs}}</div>
<div class="muted small mono">{{.LastSeenMs}}</div>
</td>
<td class="mono">{{.Version}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</form>
</div>
<script>
(() => {
const input = document.getElementById('filter');
const table = document.getElementById('devices-table');
if(!input || !table) return;
input.addEventListener('input', () => {
const q = (input.value || '').trim().toLowerCase();
const rows = table.tBodies[0].rows;
for(const tr of rows){
const txt = tr.innerText.toLowerCase();
tr.style.display = (!q || txt.includes(q)) ? '' : 'none';
}
});
})();
(() => {
const table = document.getElementById('devices-table');
const selectAll = document.getElementById('selectAll');
const out = document.getElementById('selectedCount');
if(!table || !selectAll || !out) return;
const visibleRows = () => Array.from(table.tBodies[0].rows).filter(tr => tr.style.display !== 'none');
const getChecks = (rows) => rows.map(tr => tr.querySelector('input.dev-check')).filter(Boolean);
const updateCount = () => {
const checks = getChecks(Array.from(table.tBodies[0].rows));
const n = checks.filter(c => c.checked).length;
out.textContent = String(n);
};
selectAll.addEventListener('change', () => {
const rows = visibleRows();
const checks = getChecks(rows);
for(const c of checks) c.checked = selectAll.checked;
updateCount();
});
table.addEventListener('change', (e) => {
const el = e.target;
if(el && el.classList && el.classList.contains('dev-check')) updateCount();
});
updateCount();
})();
(() => {
const els = Array.from(document.querySelectorAll('[data-media-status]'));
if(!els.length) return;
const labelFromHttp = (status, txt) => {
if(status === 501) return {cls:'warn', text:'未启用', title:txt};
if(status === 401) return {cls:'warn', text:'未授权', title:txt};
if(status === 404) return {cls:'warn', text:'未实现', title:txt};
return {cls:'warn', text:`HTTP ${status}`, title:txt};
};
async function fetchOne(el){
const id = el.getAttribute('data-media-status');
el.textContent = '查询中...';
el.className = 'pill warn';
el.title = '';
let res;
let txt = '';
try{
res = await fetch(`/api/devices/${encodeURIComponent(id)}/media-server/status`, { method: 'GET' });
txt = await res.text();
}catch(e){
el.className = 'pill warn';
el.textContent = '请求失败';
el.title = String(e);
return;
}
if(!res.ok){
const v = labelFromHttp(res.status, txt);
el.className = 'pill ' + v.cls;
el.textContent = v.text;
el.title = v.title || '';
return;
}
let obj;
try{ obj = JSON.parse(txt); }catch(e){}
if(!obj || typeof obj.running !== 'boolean'){
el.className = 'pill warn';
el.textContent = '未知';
el.title = txt;
return;
}
const pid = (typeof obj.pid === 'number' && obj.pid > 0) ? obj.pid : 0;
const cfg = (typeof obj.config_path === 'string') ? obj.config_path : '';
if(obj.running){
el.className = 'pill ok';
el.textContent = pid ? `运行中 PID ${pid}` : '运行中';
}else{
el.className = 'pill bad';
el.textContent = pid ? `未运行 PID ${pid}` : '未运行';
}
el.title = cfg;
}
const limit = 6;
let idx = 0;
const workers = Array.from({length: Math.min(limit, els.length)}, async () => {
while(true){
const i = idx++;
if(i >= els.length) return;
await fetchOne(els[i]);
}
});
Promise.all(workers);
})();
</script>
{{end}}