Просмотр исходного кода

导航重构: 报警历史合并 + 目标/姿态识别独立配置页

- 报警历史: 围栏报警+姿态报警合并一个列表, 类型标签区分, 可按类型/时间/通道筛选, 支持删除/清空
- 新增"目标识别"配置页: 跳帧/阈值/保留天数/启用开关/入侵目标
- 新增"姿态识别"配置页: 跌倒阈值/连续帧/启用开关/可报警姿态勾选(举手呼救/异常蹲坐)
- 后端: /api/pose/config 支持 pose_hand_raise/pose_crouch, /api/pose/status 返回开关状态
- 布局: sys-wrap 取消固定宽度, 配置卡片左右平铺
dev 1 неделя назад
Родитель
Сommit
8c93205216
3 измененных файлов с 245 добавлено и 167 удалено
  1. 10 0
      config/config.json
  2. 19 0
      src/main.cpp
  3. 216 167
      web/index.html

+ 10 - 0
config/config.json

@@ -164,6 +164,16 @@
             "id": 3,
             "id": 3,
             "name": "4",
             "name": "4",
             "url": "rtsp://admin:hd135792468@192.168.1.124:554/Streaming/Channels/102"
             "url": "rtsp://admin:hd135792468@192.168.1.124:554/Streaming/Channels/102"
+        },
+        {
+            "id": 4,
+            "name": "5",
+            "url": "rtsp://admin:hd135792468@192.168.1.125:554/Streaming/Channels/102"
+        },
+        {
+            "id": 5,
+            "name": "6",
+            "url": "rtsp://admin:hd135792468@192.168.1.126:554/Streaming/Channels/102"
         }
         }
     ],
     ],
     "rtsp_timeout": 10,
     "rtsp_timeout": 10,

+ 19 - 0
src/main.cpp

@@ -1527,6 +1527,8 @@ int main(int argc, char* argv[]) {
         r["fall_thresh"] = fall_thresh;
         r["fall_thresh"] = fall_thresh;
         r["fall_frames"] = fall_frames;
         r["fall_frames"] = fall_frames;
         r["fall_relays"] = fall_relays;
         r["fall_relays"] = fall_relays;
+        r["pose_hand_raise"] = pose_hand_raise;
+        r["pose_crouch"] = pose_crouch;
         json chs = json::array();
         json chs = json::array();
         {
         {
             lock_guard<mutex> lk(channels_mtx);
             lock_guard<mutex> lk(channels_mtx);
@@ -1568,6 +1570,23 @@ int main(int argc, char* argv[]) {
                     if (c.is_string()) fall_relays.push_back(c.get<string>());
                     if (c.is_string()) fall_relays.push_back(c.get<string>());
                 full["fall_relays"] = fall_relays;
                 full["fall_relays"] = fall_relays;
             }
             }
+            if (j.contains("pose_hand_raise")) {
+                bool v = j["pose_hand_raise"].get<bool>();
+                pose_hand_raise = v;
+                fallAnalyzer.setHandRaiseEnabled(v);
+                full["pose_hand_raise"] = v;
+            }
+            if (j.contains("pose_crouch")) {
+                bool v = j["pose_crouch"].get<bool>();
+                pose_crouch = v;
+                fallAnalyzer.setCrouchEnabled(v);
+                full["pose_crouch"] = v;
+            }
+            if (j.contains("pose_enabled")) {
+                bool v = j["pose_enabled"].get<bool>();
+                pose_enabled = v;
+                full["pose_enabled"] = v;
+            }
             if (j.contains("channels") && j["channels"].is_object()) {
             if (j.contains("channels") && j["channels"].is_object()) {
                 lock_guard<mutex> lk(channels_mtx);
                 lock_guard<mutex> lk(channels_mtx);
                 for (auto& [k, v] : j["channels"].items()) {
                 for (auto& [k, v] : j["channels"].items()) {

+ 216 - 167
web/index.html

@@ -120,7 +120,7 @@ border-left:3px solid #e94560;padding:8px 10px;margin-bottom:8px;cursor:pointer}
 .pill-off{background:#e74c3c;color:#fff}
 .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{margin-bottom:0;padding:12px}
 .sys-wrap .card h3{margin-bottom:8px;font-size:14px}
 .sys-wrap .card h3{margin-bottom:8px;font-size:14px}
 .sys-wrap .form-group{margin-bottom:8px}
 .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 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="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="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="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="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>
       <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="card">
         <div class="alarm-head">
         <div class="alarm-head">
           <h3>报警记录 <span id="alarmCount" style="font-size:12px;color:#888"></span></h3>
           <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>
         <div class="filter-bar">
         <div class="filter-bar">
           <div class="fitem"><label>摄像头</label>
           <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>
           <div class="fitem"><label>开始时间</label>
             <input type="datetime-local" id="fltStart"></div>
             <input type="datetime-local" id="fltStart"></div>
           <div class="fitem"><label>结束时间</label>
           <div class="fitem"><label>结束时间</label>
             <input type="datetime-local" id="fltEnd"></div>
             <input type="datetime-local" id="fltEnd"></div>
           <div class="fitem"><label>&nbsp;</label>
           <div class="fitem"><label>&nbsp;</label>
-            <button class="btn btn-primary" onclick="loadAlarms()">查询</button></div>
+            <button class="btn btn-primary" onclick="loadAllAlarms()">查询</button></div>
           <div class="fitem"><label>&nbsp;</label>
           <div class="fitem"><label>&nbsp;</label>
             <button class="btn btn-outline" onclick="resetAlarmFilters()">重置</button></div>
             <button class="btn btn-outline" onclick="resetAlarmFilters()">重置</button></div>
         </div>
         </div>
@@ -218,33 +225,50 @@ border-left:3px solid #e94560;padding:8px 10px;margin-bottom:8px;cursor:pointer}
       </div>
       </div>
     </section>
     </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>
-        <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>&nbsp;</label>
-            <button class="btn btn-primary" onclick="loadPoseAlarms()">查询</button></div>
-          <div class="fitem"><label>&nbsp;</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>
-        <div id="poseAlarmList"></div>
-        <div id="poseAlarmEmpty" class="empty-tip" style="height:200px;display:none">暂无姿态报警记录</div>
       </div>
       </div>
     </section>
     </section>
 
 
@@ -335,21 +359,6 @@ border-left:3px solid #e94560;padding:8px 10px;margin-bottom:8px;cursor:pointer}
 
 
     <section id="page-system" class="page">
     <section id="page-system" class="page">
       <div class="sys-wrap">
       <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">
         <div class="card">
           <h3>识别与输出</h3>
           <h3>识别与输出</h3>
           <div style="display:flex;gap:14px;flex-wrap:wrap;margin-bottom:8px;font-size:13px">
           <div style="display:flex;gap:14px;flex-wrap:wrap;margin-bottom:8px;font-size:13px">
@@ -460,8 +469,9 @@ function switchPage(p) {
   } else {
   } else {
     stage.style.display = 'none';
     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 === 'action') loadActions();
   if (p === 'relay') { loadRelayStatus(); loadBindings(); }
   if (p === 'relay') { loadRelayStatus(); loadBindings(); }
   if (p === 'channel') renderChannelList();
   if (p === 'channel') renderChannelList();
@@ -915,7 +925,7 @@ function syncAlarmFilterOptions() {
   }
   }
 }
 }
 
 
-/* ================= 报警历史 ================= */
+/* ================= 报警历史(围栏+姿态合并) ================= */
 function alarmQuery() {
 function alarmQuery() {
   const qs = new URLSearchParams();
   const qs = new URLSearchParams();
   const ch = document.getElementById('fltCh').value;
   const ch = document.getElementById('fltCh').value;
@@ -928,124 +938,197 @@ function alarmQuery() {
 }
 }
 function resetAlarmFilters() {
 function resetAlarmFilters() {
   document.getElementById('fltCh').value = '';
   document.getElementById('fltCh').value = '';
+  document.getElementById('fltType').value = '';
   document.getElementById('fltStart').value = '';
   document.getElementById('fltStart').value = '';
   document.getElementById('fltEnd').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 {
   try {
     const qs = alarmQuery();
     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 =
     document.getElementById('alarmCount').textContent =
-      d.total + '条' + (qs ? '(筛选后)' : '');
+      items.length + '条' + (qs ? '(筛选后)' : '');
     const list = document.getElementById('alarmList');
     const list = document.getElementById('alarmList');
     const empty = document.getElementById('alarmEmpty');
     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';
     empty.style.display = 'none';
-    list.innerHTML = d.items.map(a => {
+    list.innerHTML = items.map(a => {
       const c = chById(a.channel);
       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 `
       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="">
         <img src="/${a.screenshot}" loading="lazy" alt="">
         <div class="col-t">${a.timestamp}</div>
         <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-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>`;
       </div>`;
     }).join('');
     }).join('');
   } catch (e) {}
   } catch (e) {}
 }
 }
-function showAlarm(screenshot, time, ch, fence, cls, conf) {
+function showAlarm(screenshot, time, ch, label, detail, conf) {
   document.getElementById('alarmScreenshot').src = '/' + screenshot;
   document.getElementById('alarmScreenshot').src = '/' + screenshot;
   const c = chById(ch);
   const c = chById(ch);
   document.getElementById('alarmMeta').innerHTML =
   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');
   document.getElementById('alarmModal').classList.add('show');
 }
 }
 function closeAlarmModal() { document.getElementById('alarmModal').classList.remove('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;
   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 {
   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();
     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) {}
   } 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 {
   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) {}
   } 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 {
   try {
     const r = await fetch('/api/status');
     const r = await fetch('/api/status');
     const d = await r.json();
     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('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('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('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';
     if (document.getElementById('sysSteelOutput')) document.getElementById('sysSteelOutput').value = d.steel_output === false ? 'false' : 'true';
-    setSelectedClasses(d.alarm_classes);
   } catch (e) {}
   } catch (e) {}
   try {
   try {
     const r = await fetch('/api/mjpeg/status');
     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';
     if (document.getElementById('sysMjpegEnabled')) document.getElementById('sysMjpegEnabled').value = d.enabled === false ? 'false' : 'true';
   } catch (e) {}
   } 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() {
 async function loadSystemConfig() {
   try {
   try {
@@ -1541,13 +1589,14 @@ renderChannelList();
 loadStatus();
 loadStatus();
 loadFences();
 loadFences();
 loadAuthInfo();
 loadAuthInfo();
-loadConfig();
+loadSystemOptions();
 loadSystemConfig();
 loadSystemConfig();
+loadTargetConfig();
+loadPoseConfig();
 loadRelayStatus();
 loadRelayStatus();
 loadBindings();
 loadBindings();
 setInterval(loadStatus, 5000);
 setInterval(loadStatus, 5000);
-setInterval(() => { if (curPage === 'alarm') loadAlarms(); }, 10000);
-setInterval(() => { if (curPage === 'posealarm') loadPoseAlarms(); }, 10000);
+setInterval(() => { if (curPage === 'alarm') loadAllAlarms(); }, 10000);
 setInterval(loadRelayStatus, 1000);
 setInterval(loadRelayStatus, 1000);
 </script>
 </script>
 </body>
 </body>