|
|
@@ -120,7 +120,7 @@ border-left:3px solid #e94560;padding:8px 10px;margin-bottom:8px;cursor:pointer}
|
|
|
.pill-off{background:#e74c3c;color:#fff}
|
|
|
|
|
|
/* ---------- 系统页: 两列紧凑布局 ---------- */
|
|
|
-.sys-wrap{max-width:1040px;margin:0 auto;padding:12px;display:grid;grid-template-columns:1fr 1fr;gap:12px;align-content:start}
|
|
|
+.sys-wrap{margin:0 auto;padding:12px;display:grid;grid-template-columns:1fr 1fr;gap:12px;align-content:start}
|
|
|
.sys-wrap .card{margin-bottom:0;padding:12px}
|
|
|
.sys-wrap .card h3{margin-bottom:8px;font-size:14px}
|
|
|
.sys-wrap .form-group{margin-bottom:8px}
|
|
|
@@ -150,7 +150,8 @@ border-left:3px solid #e94560;padding:8px 10px;margin-bottom:8px;cursor:pointer}
|
|
|
<div class="nav-item active" data-page="monitor" onclick="switchPage('monitor')"><span class="ico">监</span><span class="nav-label">实时监控</span></div>
|
|
|
<div class="nav-item" data-page="fence" onclick="switchPage('fence')"><span class="ico">围</span><span class="nav-label">电子围栏</span></div>
|
|
|
<div class="nav-item" data-page="alarm" onclick="switchPage('alarm')"><span class="ico">警</span><span class="nav-label">报警历史</span></div>
|
|
|
- <div class="nav-item" data-page="posealarm" onclick="switchPage('posealarm')"><span class="ico">姿</span><span class="nav-label">姿态报警</span></div>
|
|
|
+ <div class="nav-item" data-page="targetcfg" onclick="switchPage('targetcfg')"><span class="ico">目</span><span class="nav-label">目标识别</span></div>
|
|
|
+ <div class="nav-item" data-page="posecfg" onclick="switchPage('posecfg')"><span class="ico">姿</span><span class="nav-label">姿态识别</span></div>
|
|
|
<div class="nav-item" data-page="action" onclick="switchPage('action')"><span class="ico">动</span><span class="nav-label">动作记录</span></div>
|
|
|
<div class="nav-item" data-page="relay" onclick="switchPage('relay')"><span class="ico">继</span><span class="nav-label">继电器控制</span></div>
|
|
|
<div class="nav-item" data-page="channel" onclick="switchPage('channel')"><span class="ico">通</span><span class="nav-label">通道管理</span></div>
|
|
|
@@ -199,17 +200,23 @@ border-left:3px solid #e94560;padding:8px 10px;margin-bottom:8px;cursor:pointer}
|
|
|
<div class="card">
|
|
|
<div class="alarm-head">
|
|
|
<h3>报警记录 <span id="alarmCount" style="font-size:12px;color:#888"></span></h3>
|
|
|
- <button class="btn btn-danger" style="padding:5px 14px" onclick="clearAlarms()">清空全部</button>
|
|
|
+ <button class="btn btn-danger" style="padding:5px 14px" onclick="clearAllAlarms()">清空全部</button>
|
|
|
</div>
|
|
|
<div class="filter-bar">
|
|
|
<div class="fitem"><label>摄像头</label>
|
|
|
- <select id="fltCh" onchange="loadAlarms()"><option value="">全部</option></select></div>
|
|
|
+ <select id="fltCh" onchange="loadAllAlarms()"><option value="">全部</option></select></div>
|
|
|
+ <div class="fitem"><label>报警类型</label>
|
|
|
+ <select id="fltType" onchange="loadAllAlarms()">
|
|
|
+ <option value="">全部</option>
|
|
|
+ <option value="fence">围栏报警</option>
|
|
|
+ <option value="pose">姿态报警</option>
|
|
|
+ </select></div>
|
|
|
<div class="fitem"><label>开始时间</label>
|
|
|
<input type="datetime-local" id="fltStart"></div>
|
|
|
<div class="fitem"><label>结束时间</label>
|
|
|
<input type="datetime-local" id="fltEnd"></div>
|
|
|
<div class="fitem"><label> </label>
|
|
|
- <button class="btn btn-primary" onclick="loadAlarms()">查询</button></div>
|
|
|
+ <button class="btn btn-primary" onclick="loadAllAlarms()">查询</button></div>
|
|
|
<div class="fitem"><label> </label>
|
|
|
<button class="btn btn-outline" onclick="resetAlarmFilters()">重置</button></div>
|
|
|
</div>
|
|
|
@@ -218,33 +225,50 @@ border-left:3px solid #e94560;padding:8px 10px;margin-bottom:8px;cursor:pointer}
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
- <section id="page-posealarm" class="page pad">
|
|
|
- <div class="card">
|
|
|
- <div class="alarm-head">
|
|
|
- <h3>姿态报警记录 <span id="poseAlarmCount" style="font-size:12px;color:#888"></span></h3>
|
|
|
- <button class="btn btn-danger" style="padding:5px 14px" onclick="clearPoseAlarms()">清空全部</button>
|
|
|
+ <section id="page-targetcfg" class="page pad">
|
|
|
+ <div class="sys-wrap">
|
|
|
+ <div class="card">
|
|
|
+ <h3>基础参数</h3>
|
|
|
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
|
+ <div class="form-group" style="margin:0"><label>跳帧数(N帧推理一次)</label><input type="number" id="cfgSkip" min="0" max="30" value="3"></div>
|
|
|
+ <div class="form-group" style="margin:0"><label>置信度阈值</label><input type="number" id="cfgConf" min="0.1" max="1" step="0.05" value="0.35"></div>
|
|
|
+ <div class="form-group" style="margin:0"><label>报警保留天数</label><input type="number" id="cfgRetention" min="1" max="3650" value="30"></div>
|
|
|
+ </div>
|
|
|
+ <div class="form-group" style="margin-top:12px">
|
|
|
+ <label style="display:flex;align-items:center;gap:5px;cursor:pointer"><input type="checkbox" id="cfgTargetEnabled"> 启用目标识别</label>
|
|
|
+ </div>
|
|
|
+ <div class="btn-row" style="margin-top:8px"><button class="btn btn-primary" onclick="saveConfig()">保存参数</button></div>
|
|
|
</div>
|
|
|
- <div class="filter-bar">
|
|
|
- <div class="fitem"><label>摄像头</label>
|
|
|
- <select id="pfltCh" onchange="loadPoseAlarms()"><option value="">全部</option></select></div>
|
|
|
- <div class="fitem"><label>动作类型</label>
|
|
|
- <select id="pfltAction" onchange="loadPoseAlarms()">
|
|
|
- <option value="">全部</option>
|
|
|
- <option value="跌倒">跌倒</option>
|
|
|
- <option value="举手呼救">举手呼救</option>
|
|
|
- <option value="异常蹲坐">异常蹲坐</option>
|
|
|
- </select></div>
|
|
|
- <div class="fitem"><label>开始时间</label>
|
|
|
- <input type="datetime-local" id="pfltStart"></div>
|
|
|
- <div class="fitem"><label>结束时间</label>
|
|
|
- <input type="datetime-local" id="pfltEnd"></div>
|
|
|
- <div class="fitem"><label> </label>
|
|
|
- <button class="btn btn-primary" onclick="loadPoseAlarms()">查询</button></div>
|
|
|
- <div class="fitem"><label> </label>
|
|
|
- <button class="btn btn-outline" onclick="resetPoseFilters()">重置</button></div>
|
|
|
+ <div class="card">
|
|
|
+ <h3>入侵目标</h3>
|
|
|
+ <div class="form-group">
|
|
|
+ <label>勾选后进入围栏才报警</label>
|
|
|
+ <div id="targetList" style="display:grid;grid-template-columns:1fr 1fr;gap:8px 16px;font-size:13px"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section id="page-posecfg" class="page pad">
|
|
|
+ <div class="sys-wrap">
|
|
|
+ <div class="card">
|
|
|
+ <h3>基础参数</h3>
|
|
|
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
|
+ <div class="form-group" style="margin:0"><label>跌倒阈值</label><input type="number" id="cfgFallThresh" min="0" max="1" step="0.05" value="0.5"></div>
|
|
|
+ <div class="form-group" style="margin:0"><label>连续确认帧数</label><input type="number" id="cfgFallFrames" min="1" max="30" value="3"></div>
|
|
|
+ </div>
|
|
|
+ <div class="form-group" style="margin-top:12px">
|
|
|
+ <label style="display:flex;align-items:center;gap:5px;cursor:pointer"><input type="checkbox" id="cfgPoseEnabled"> 启用姿态识别</label>
|
|
|
+ </div>
|
|
|
+ <div class="btn-row" style="margin-top:8px"><button class="btn btn-primary" onclick="savePoseConfig()">保存姿态配置</button></div>
|
|
|
+ </div>
|
|
|
+ <div class="card">
|
|
|
+ <h3>可报警姿态</h3>
|
|
|
+ <div class="form-group">
|
|
|
+ <label>勾选启用对应姿态报警</label>
|
|
|
+ <div id="poseActionList" style="display:grid;grid-template-columns:1fr 1fr;gap:8px 16px;font-size:13px"></div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div id="poseAlarmList"></div>
|
|
|
- <div id="poseAlarmEmpty" class="empty-tip" style="height:200px;display:none">暂无姿态报警记录</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
@@ -335,21 +359,6 @@ border-left:3px solid #e94560;padding:8px 10px;margin-bottom:8px;cursor:pointer}
|
|
|
|
|
|
<section id="page-system" class="page">
|
|
|
<div class="sys-wrap">
|
|
|
- <div class="card">
|
|
|
- <h3>检测参数</h3>
|
|
|
- <div style="display:flex;gap:10px">
|
|
|
- <div class="form-group" style="flex:1"><label>跳帧数(N帧推理一次)</label><input type="number" id="cfgSkip" min="0" max="30" value="3"></div>
|
|
|
- <div class="form-group" style="flex:1"><label>置信度阈值</label><input type="number" id="cfgConf" min="0.1" max="1" step="0.05" value="0.35"></div>
|
|
|
- <div class="form-group" style="flex:1"><label>报警保留天数</label><input type="number" id="cfgRetention" min="1" max="3650" value="30"></div>
|
|
|
- </div>
|
|
|
- <div class="form-group">
|
|
|
- <label>入侵目标 (勾选后进入围栏才报警)</label>
|
|
|
- <div id="targetList" style="display:flex;flex-wrap:wrap;gap:6px 16px;font-size:13px"></div>
|
|
|
- </div>
|
|
|
- <div class="btn-row" style="margin-top:0">
|
|
|
- <button class="btn btn-primary" onclick="saveConfig()">保存参数</button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
<div class="card">
|
|
|
<h3>识别与输出</h3>
|
|
|
<div style="display:flex;gap:14px;flex-wrap:wrap;margin-bottom:8px;font-size:13px">
|
|
|
@@ -460,8 +469,9 @@ function switchPage(p) {
|
|
|
} else {
|
|
|
stage.style.display = 'none';
|
|
|
}
|
|
|
- if (p === 'alarm') loadAlarms();
|
|
|
- if (p === 'posealarm') loadPoseAlarms();
|
|
|
+ if (p === 'alarm') loadAllAlarms();
|
|
|
+ if (p === 'targetcfg') loadTargetConfig();
|
|
|
+ if (p === 'posecfg') loadPoseConfig();
|
|
|
if (p === 'action') loadActions();
|
|
|
if (p === 'relay') { loadRelayStatus(); loadBindings(); }
|
|
|
if (p === 'channel') renderChannelList();
|
|
|
@@ -915,7 +925,7 @@ function syncAlarmFilterOptions() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/* ================= 报警历史 ================= */
|
|
|
+/* ================= 报警历史(围栏+姿态合并) ================= */
|
|
|
function alarmQuery() {
|
|
|
const qs = new URLSearchParams();
|
|
|
const ch = document.getElementById('fltCh').value;
|
|
|
@@ -928,124 +938,197 @@ function alarmQuery() {
|
|
|
}
|
|
|
function resetAlarmFilters() {
|
|
|
document.getElementById('fltCh').value = '';
|
|
|
+ document.getElementById('fltType').value = '';
|
|
|
document.getElementById('fltStart').value = '';
|
|
|
document.getElementById('fltEnd').value = '';
|
|
|
- loadAlarms();
|
|
|
+ loadAllAlarms();
|
|
|
}
|
|
|
-
|
|
|
-async function loadAlarms() {
|
|
|
+function poseActionColor(a) {
|
|
|
+ if (a === '跌倒') return '#e94560';
|
|
|
+ if (a === '举手呼救') return '#f39c12';
|
|
|
+ if (a === '异常蹲坐') return '#3498db';
|
|
|
+ return '#8892a8';
|
|
|
+}
|
|
|
+async function loadAllAlarms() {
|
|
|
try {
|
|
|
const qs = alarmQuery();
|
|
|
- const r = await fetch('/api/alarms' + (qs ? '?' + qs : ''));
|
|
|
- if (r.status === 401) { location.href = '/login'; return; }
|
|
|
- const d = await r.json();
|
|
|
+ const type = document.getElementById('fltType').value;
|
|
|
+ // 并行取围栏报警 + 姿态报警, 合并后按时间排序
|
|
|
+ const [fenceR, poseR] = await Promise.all([
|
|
|
+ fetch('/api/alarms' + (qs ? '?' + qs : '')),
|
|
|
+ fetch('/api/pose/alarms' + (qs ? '?' + qs : ''))
|
|
|
+ ]);
|
|
|
+ if (fenceR.status === 401) { location.href = '/login'; return; }
|
|
|
+ const fd = await fenceR.json();
|
|
|
+ const pd = await poseR.json();
|
|
|
+ const fenceItems = (fd.items || []).map(a => ({
|
|
|
+ type: 'fence',
|
|
|
+ timestamp: a.timestamp, channel: a.channel,
|
|
|
+ label: a.fence_name, detail: a.class_name,
|
|
|
+ conf: a.confidence, screenshot: a.screenshot,
|
|
|
+ actionColor: '#27ae60', id: a.id
|
|
|
+ }));
|
|
|
+ const poseItems = (pd.items || []).map(a => ({
|
|
|
+ type: 'pose',
|
|
|
+ timestamp: a.timestamp, channel: a.channel,
|
|
|
+ label: a.action, detail: '姿态',
|
|
|
+ conf: a.score, screenshot: a.screenshot,
|
|
|
+ actionColor: poseActionColor(a.action), id: a.id
|
|
|
+ }));
|
|
|
+ let items = type === 'fence' ? fenceItems
|
|
|
+ : type === 'pose' ? poseItems
|
|
|
+ : [...fenceItems, ...poseItems];
|
|
|
+ items.sort((x, y) => y.timestamp.localeCompare(x.timestamp));
|
|
|
document.getElementById('alarmCount').textContent =
|
|
|
- d.total + '条' + (qs ? '(筛选后)' : '');
|
|
|
+ items.length + '条' + (qs ? '(筛选后)' : '');
|
|
|
const list = document.getElementById('alarmList');
|
|
|
const empty = document.getElementById('alarmEmpty');
|
|
|
- if (!d.items.length) {
|
|
|
- list.innerHTML = ''; empty.style.display = 'flex'; return;
|
|
|
- }
|
|
|
+ if (!items.length) { list.innerHTML = ''; empty.style.display = 'flex'; return; }
|
|
|
empty.style.display = 'none';
|
|
|
- list.innerHTML = d.items.map(a => {
|
|
|
+ list.innerHTML = items.map(a => {
|
|
|
const c = chById(a.channel);
|
|
|
+ const tag = a.type === 'pose'
|
|
|
+ ? `<span style="color:${a.actionColor};font-size:11px;border:1px solid ${a.actionColor};padding:0 6px;border-radius:3px">姿态</span>`
|
|
|
+ : `<span style="color:#27ae60;font-size:11px;border:1px solid #27ae60;padding:0 6px;border-radius:3px">围栏</span>`;
|
|
|
return `
|
|
|
- <div class="alist-row" onclick="showAlarm('${a.screenshot}','${a.timestamp}',${a.channel},'${a.fence_name}','${a.class_name}',${a.confidence})">
|
|
|
+ <div class="alist-row" style="border-left-color:${a.actionColor}"
|
|
|
+ onclick="showAlarm('${a.screenshot}','${a.timestamp}',${a.channel},'${a.label}','${a.detail}',${a.conf})">
|
|
|
<img src="/${a.screenshot}" loading="lazy" alt="">
|
|
|
<div class="col-t">${a.timestamp}</div>
|
|
|
<div class="col-ch">CH${a.channel}<div class="nm">${c ? c.name : ''}</div></div>
|
|
|
- <div class="col-f">${a.fence_name}</div>
|
|
|
- <div class="col-c">${a.class_name}</div>
|
|
|
- <div class="col-conf">${(a.confidence*100).toFixed(0)}%</div>
|
|
|
+ <div class="col-f" style="color:${a.actionColor}">${a.label}</div>
|
|
|
+ <div class="col-c">${tag}</div>
|
|
|
+ <div class="col-conf">${(a.conf*100).toFixed(0)}%</div>
|
|
|
+ <button class="btn btn-outline" style="padding:3px 10px;font-size:12px"
|
|
|
+ onclick="event.stopPropagation();delAlarm(${a.id},'${a.type}')">删除</button>
|
|
|
</div>`;
|
|
|
}).join('');
|
|
|
} catch (e) {}
|
|
|
}
|
|
|
-function showAlarm(screenshot, time, ch, fence, cls, conf) {
|
|
|
+function showAlarm(screenshot, time, ch, label, detail, conf) {
|
|
|
document.getElementById('alarmScreenshot').src = '/' + screenshot;
|
|
|
const c = chById(ch);
|
|
|
document.getElementById('alarmMeta').innerHTML =
|
|
|
- `时间: ${time}<br>通道: CH${ch} ${c ? c.name : ''}<br>围栏: ${fence}<br>目标: ${cls}<br>置信度: ${(conf*100).toFixed(1)}%`;
|
|
|
+ `时间: ${time}<br>通道: CH${ch} ${c ? c.name : ''}<br>类型: ${label}<br>详情: ${detail}<br>置信度: ${(conf*100).toFixed(1)}%`;
|
|
|
document.getElementById('alarmModal').classList.add('show');
|
|
|
}
|
|
|
function closeAlarmModal() { document.getElementById('alarmModal').classList.remove('show'); }
|
|
|
-async function clearAlarms() {
|
|
|
+async function delAlarm(id, type) {
|
|
|
+ if (!confirm('删除此条报警?')) return;
|
|
|
+ const url = type === 'pose' ? '/api/pose/alarms/delete' : '/api/alarms/delete';
|
|
|
+ try {
|
|
|
+ await fetch(url, {method:'POST', headers:{'Content-Type':'application/json'},
|
|
|
+ body: JSON.stringify({id})});
|
|
|
+ loadAllAlarms();
|
|
|
+ } catch (e) {}
|
|
|
+}
|
|
|
+async function clearAllAlarms() {
|
|
|
if (!confirm('清空所有报警记录?')) return;
|
|
|
- try { await fetch('/api/alarms/clear', {method:'POST'}); loadAlarms(); } catch (e) {}
|
|
|
+ try {
|
|
|
+ await fetch('/api/alarms/clear', {method:'POST'});
|
|
|
+ await fetch('/api/pose/alarms/clear', {method:'POST'});
|
|
|
+ loadAllAlarms();
|
|
|
+ } catch (e) {}
|
|
|
}
|
|
|
|
|
|
-/* ================= 姿态报警历史 ================= */
|
|
|
-function poseAlarmQuery() {
|
|
|
- const qs = new URLSearchParams();
|
|
|
- const ch = document.getElementById('pfltCh').value;
|
|
|
- if (ch !== '') qs.set('channel', ch);
|
|
|
- const act = document.getElementById('pfltAction').value;
|
|
|
- if (act) qs.set('action', act);
|
|
|
- const st = document.getElementById('pfltStart').value.replace('T', ' ');
|
|
|
- const en = document.getElementById('pfltEnd').value.replace('T', ' ');
|
|
|
- if (st) qs.set('start', st + ':00');
|
|
|
- if (en) qs.set('end', en + ':59');
|
|
|
- return qs.toString();
|
|
|
+/* ================= 目标识别配置 ================= */
|
|
|
+const TARGET_CLASSES = [
|
|
|
+ ['person','人'], ['bicycle','自行车'], ['car','汽车'],
|
|
|
+ ['motorcycle','摩托车'], ['bus','公交车'], ['truck','卡车'], ['train','火车']
|
|
|
+];
|
|
|
+function renderTargets() {
|
|
|
+ const el = document.getElementById('targetList');
|
|
|
+ if (!el) return;
|
|
|
+ el.innerHTML = TARGET_CLASSES.map(([name,label]) =>
|
|
|
+ `<label style="cursor:pointer;display:flex;align-items:center;gap:4px">
|
|
|
+ <input type="checkbox" class="target-ck" value="${name}">${label}</label>`).join('');
|
|
|
}
|
|
|
-function resetPoseFilters() {
|
|
|
- document.getElementById('pfltCh').value = '';
|
|
|
- document.getElementById('pfltAction').value = '';
|
|
|
- document.getElementById('pfltStart').value = '';
|
|
|
- document.getElementById('pfltEnd').value = '';
|
|
|
- loadPoseAlarms();
|
|
|
+function getSelectedClasses() {
|
|
|
+ return Array.from(document.querySelectorAll('.target-ck:checked')).map(c => c.value);
|
|
|
}
|
|
|
-function poseActionColor(a) {
|
|
|
- if (a === '跌倒') return '#e94560';
|
|
|
- if (a === '举手呼救') return '#f39c12';
|
|
|
- if (a === '异常蹲坐') return '#3498db';
|
|
|
- return '#8892a8';
|
|
|
+function setSelectedClasses(list) {
|
|
|
+ const set = new Set(list || []);
|
|
|
+ document.querySelectorAll('.target-ck').forEach(c => c.checked = set.has(c.value));
|
|
|
}
|
|
|
-async function loadPoseAlarms() {
|
|
|
+async function loadTargetConfig() {
|
|
|
try {
|
|
|
- const qs = poseAlarmQuery();
|
|
|
- const r = await fetch('/api/pose/alarms' + (qs ? '?' + qs : ''));
|
|
|
- if (r.status === 401) { location.href = '/login'; return; }
|
|
|
+ const r = await fetch('/api/status');
|
|
|
const d = await r.json();
|
|
|
- document.getElementById('poseAlarmCount').textContent =
|
|
|
- d.total + '条' + (qs ? '(筛选后)' : '');
|
|
|
- const list = document.getElementById('poseAlarmList');
|
|
|
- const empty = document.getElementById('poseAlarmEmpty');
|
|
|
- if (!d.items.length) { list.innerHTML = ''; empty.style.display = 'flex'; return; }
|
|
|
- empty.style.display = 'none';
|
|
|
- list.innerHTML = d.items.map(a => {
|
|
|
- const c = chById(a.channel);
|
|
|
- return `
|
|
|
- <div class="alist-row" style="border-left-color:${poseActionColor(a.action)}"
|
|
|
- onclick="showPoseAlarm('${a.screenshot}','${a.timestamp}',${a.channel},'${a.action}',${a.score},${a.id})">
|
|
|
- <img src="/${a.screenshot}" loading="lazy" alt="">
|
|
|
- <div class="col-t">${a.timestamp}</div>
|
|
|
- <div class="col-ch">CH${a.channel}<div class="nm">${c ? c.name : ''}</div></div>
|
|
|
- <div class="col-f" style="color:${poseActionColor(a.action)}">${a.action}</div>
|
|
|
- <div class="col-conf">${(a.score*100).toFixed(0)}%</div>
|
|
|
- <button class="btn btn-outline" style="padding:3px 10px;font-size:12px"
|
|
|
- onclick="event.stopPropagation();delPoseAlarm(${a.id})">删除</button>
|
|
|
- </div>`;
|
|
|
- }).join('');
|
|
|
+ if (document.getElementById('cfgSkip')) document.getElementById('cfgSkip').value = d.skip_frames;
|
|
|
+ if (document.getElementById('cfgConf')) document.getElementById('cfgConf').value = d.conf_thresh;
|
|
|
+ if (document.getElementById('cfgRetention')) document.getElementById('cfgRetention').value = d.alarm_retention_days;
|
|
|
+ if (document.getElementById('cfgTargetEnabled')) document.getElementById('cfgTargetEnabled').checked = d.target_enabled !== false;
|
|
|
+ renderTargets();
|
|
|
+ setSelectedClasses(d.alarm_classes);
|
|
|
} catch (e) {}
|
|
|
}
|
|
|
-function showPoseAlarm(screenshot, time, ch, action, score, id) {
|
|
|
- document.getElementById('alarmScreenshot').src = '/' + screenshot;
|
|
|
- const c = chById(ch);
|
|
|
- document.getElementById('alarmMeta').innerHTML =
|
|
|
- `时间: ${time}<br>通道: CH${ch} ${c ? c.name : ''}<br>动作: <span style="color:${poseActionColor(action)}">${action}</span><br>得分: ${(score*100).toFixed(1)}%`;
|
|
|
- document.getElementById('alarmModal').classList.add('show');
|
|
|
+async function saveConfig() {
|
|
|
+ const body = {
|
|
|
+ skip_frames: parseInt(document.getElementById('cfgSkip').value),
|
|
|
+ conf_thresh: parseFloat(document.getElementById('cfgConf').value),
|
|
|
+ alarm_retention_days: parseInt(document.getElementById('cfgRetention').value),
|
|
|
+ alarm_classes: getSelectedClasses(),
|
|
|
+ target_enabled: document.getElementById('cfgTargetEnabled').checked
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ const r = await fetch('/api/config', {method:'POST',
|
|
|
+ headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
|
|
|
+ const d = await r.json();
|
|
|
+ alert(d.ok ? '已保存' : '保存失败');
|
|
|
+ } catch (e) { alert('保存失败'); }
|
|
|
+}
|
|
|
+
|
|
|
+/* ================= 姿态识别配置 ================= */
|
|
|
+const POSE_ACTIONS = [
|
|
|
+ ['跌倒', '跌倒'],
|
|
|
+ ['举手呼救', '举手呼救'],
|
|
|
+ ['异常蹲坐', '异常蹲坐']
|
|
|
+];
|
|
|
+function renderPoseActions() {
|
|
|
+ const el = document.getElementById('poseActionList');
|
|
|
+ if (!el) return;
|
|
|
+ el.innerHTML = POSE_ACTIONS.map(([name, label]) =>
|
|
|
+ `<label style="cursor:pointer;display:flex;align-items:center;gap:4px">
|
|
|
+ <input type="checkbox" class="pose-act-ck" value="${name}">${label}</label>`).join('');
|
|
|
}
|
|
|
-async function delPoseAlarm(id) {
|
|
|
- if (!confirm('删除此条姿态报警?')) return;
|
|
|
+async function loadPoseConfig() {
|
|
|
try {
|
|
|
- await fetch('/api/pose/alarms/delete', {method:'POST',
|
|
|
- headers:{'Content-Type':'application/json'}, body:JSON.stringify({id})});
|
|
|
- loadPoseAlarms();
|
|
|
+ const [r1, r2] = await Promise.all([
|
|
|
+ fetch('/api/status'),
|
|
|
+ fetch('/api/pose/status')
|
|
|
+ ]);
|
|
|
+ const d = await r1.json();
|
|
|
+ const pd = await r2.json();
|
|
|
+ if (document.getElementById('cfgPoseEnabled')) document.getElementById('cfgPoseEnabled').checked = d.pose_cfg_enabled !== false;
|
|
|
+ if (document.getElementById('cfgFallThresh')) document.getElementById('cfgFallThresh').value = pd.fall_thresh;
|
|
|
+ if (document.getElementById('cfgFallFrames')) document.getElementById('cfgFallFrames').value = pd.fall_frames;
|
|
|
+ // 可报警姿态: 固定渲染4类, 由后端开关决定勾选
|
|
|
+ renderPoseActions();
|
|
|
+ const enabled = [];
|
|
|
+ if (pd.pose_hand_raise !== false) enabled.push('举手呼救');
|
|
|
+ if (pd.pose_crouch !== false) enabled.push('异常蹲坐');
|
|
|
+ // 跌倒固定启用
|
|
|
+ enabled.push('跌倒');
|
|
|
+ document.querySelectorAll('.pose-act-ck').forEach(c =>
|
|
|
+ c.checked = enabled.includes(c.value));
|
|
|
} catch (e) {}
|
|
|
}
|
|
|
-async function clearPoseAlarms() {
|
|
|
- if (!confirm('清空所有姿态报警记录?')) return;
|
|
|
- try { await fetch('/api/pose/alarms/clear', {method:'POST'}); loadPoseAlarms(); } catch (e) {}
|
|
|
+async function savePoseConfig() {
|
|
|
+ const checks = {};
|
|
|
+ document.querySelectorAll('.pose-act-ck').forEach(c => checks[c.value] = c.checked);
|
|
|
+ const body = {
|
|
|
+ pose_enabled: document.getElementById('cfgPoseEnabled').checked,
|
|
|
+ fall_thresh: parseFloat(document.getElementById('cfgFallThresh').value),
|
|
|
+ fall_frames: parseInt(document.getElementById('cfgFallFrames').value),
|
|
|
+ pose_hand_raise: !!checks['举手呼救'],
|
|
|
+ pose_crouch: !!checks['异常蹲坐']
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ const r = await fetch('/api/pose/config', {method:'POST',
|
|
|
+ headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
|
|
|
+ const d = await r.json();
|
|
|
+ alert(d.ok ? '已保存' : '保存失败');
|
|
|
+ } catch (e) { alert('保存失败'); }
|
|
|
}
|
|
|
|
|
|
/* ================= 继电器动作记录 ================= */
|
|
|
@@ -1357,34 +1440,14 @@ async function saveBindings() {
|
|
|
}
|
|
|
|
|
|
/* ================= 检测参数 / 系统设置 ================= */
|
|
|
-const TARGET_CLASSES = [
|
|
|
- ['person','人'], ['bicycle','自行车'], ['car','汽车'],
|
|
|
- ['motorcycle','摩托车'], ['bus','公交车'], ['truck','卡车'], ['train','火车']
|
|
|
-];
|
|
|
-function renderTargets() {
|
|
|
- document.getElementById('targetList').innerHTML = TARGET_CLASSES.map(([name,label]) =>
|
|
|
- `<label style="cursor:pointer;display:flex;align-items:center;gap:4px">
|
|
|
- <input type="checkbox" class="target-ck" value="${name}">${label}</label>`).join('');
|
|
|
-}
|
|
|
-function getSelectedClasses() {
|
|
|
- return Array.from(document.querySelectorAll('.target-ck:checked')).map(c => c.value);
|
|
|
-}
|
|
|
-function setSelectedClasses(list) {
|
|
|
- const set = new Set(list || []);
|
|
|
- document.querySelectorAll('.target-ck').forEach(c => c.checked = set.has(c.value));
|
|
|
-}
|
|
|
-async function loadConfig() {
|
|
|
+async function loadSystemOptions() {
|
|
|
try {
|
|
|
const r = await fetch('/api/status');
|
|
|
const d = await r.json();
|
|
|
- if (d.skip_frames !== undefined) document.getElementById('cfgSkip').value = d.skip_frames;
|
|
|
- if (d.conf_thresh !== undefined) document.getElementById('cfgConf').value = d.conf_thresh;
|
|
|
- if (d.alarm_retention_days !== undefined) document.getElementById('cfgRetention').value = d.alarm_retention_days;
|
|
|
if (document.getElementById('sysTargetEnabled')) document.getElementById('sysTargetEnabled').checked = d.target_enabled !== false;
|
|
|
if (document.getElementById('sysPoseEnabled')) document.getElementById('sysPoseEnabled').checked = d.pose_cfg_enabled !== false;
|
|
|
if (document.getElementById('sysGridOutput')) document.getElementById('sysGridOutput').value = d.grid_output === false ? 'false' : 'true';
|
|
|
if (document.getElementById('sysSteelOutput')) document.getElementById('sysSteelOutput').value = d.steel_output === false ? 'false' : 'true';
|
|
|
- setSelectedClasses(d.alarm_classes);
|
|
|
} catch (e) {}
|
|
|
try {
|
|
|
const r = await fetch('/api/mjpeg/status');
|
|
|
@@ -1392,21 +1455,6 @@ async function loadConfig() {
|
|
|
if (document.getElementById('sysMjpegEnabled')) document.getElementById('sysMjpegEnabled').value = d.enabled === false ? 'false' : 'true';
|
|
|
} catch (e) {}
|
|
|
}
|
|
|
-async function saveConfig() {
|
|
|
- const body = {
|
|
|
- skip_frames: parseInt(document.getElementById('cfgSkip').value),
|
|
|
- conf_thresh: parseFloat(document.getElementById('cfgConf').value),
|
|
|
- alarm_retention_days: parseInt(document.getElementById('cfgRetention').value),
|
|
|
- alarm_classes: getSelectedClasses()
|
|
|
- };
|
|
|
- try {
|
|
|
- await fetch('/api/config', {
|
|
|
- method:'POST', headers:{'Content-Type':'application/json'},
|
|
|
- body: JSON.stringify(body)
|
|
|
- });
|
|
|
- alert('已保存');
|
|
|
- } catch (e) { alert('保存失败'); }
|
|
|
-}
|
|
|
|
|
|
async function loadSystemConfig() {
|
|
|
try {
|
|
|
@@ -1541,13 +1589,14 @@ renderChannelList();
|
|
|
loadStatus();
|
|
|
loadFences();
|
|
|
loadAuthInfo();
|
|
|
-loadConfig();
|
|
|
+loadSystemOptions();
|
|
|
loadSystemConfig();
|
|
|
+loadTargetConfig();
|
|
|
+loadPoseConfig();
|
|
|
loadRelayStatus();
|
|
|
loadBindings();
|
|
|
setInterval(loadStatus, 5000);
|
|
|
-setInterval(() => { if (curPage === 'alarm') loadAlarms(); }, 10000);
|
|
|
-setInterval(() => { if (curPage === 'posealarm') loadPoseAlarms(); }, 10000);
|
|
|
+setInterval(() => { if (curPage === 'alarm') loadAllAlarms(); }, 10000);
|
|
|
setInterval(loadRelayStatus, 1000);
|
|
|
</script>
|
|
|
</body>
|