Selaa lähdekoodia

手机报警历史增加分页下拉加载

- 首次加载20条报警记录
- 滚动到底部自动加载下一页
- 围栏/姿态报警合并后统一分页
- 筛选条件变化自动回到第一页
- 列表每10秒刷新
dev 6 päivää sitten
vanhempi
commit
a7ffbac399
1 muutettua tiedostoa jossa 53 lisäystä ja 25 poistoa
  1. 53 25
      web/mobile.html

+ 53 - 25
web/mobile.html

@@ -38,6 +38,7 @@ body{font-family:system-ui,sans-serif;background:#0b1220;color:#e8e8e8;height:10
 
 /* 报警页 */
 #alarmPage{flex:1;overflow-y:auto;padding:12px}
+#alarmMore{text-align:center;color:#8892a8;font-size:13px;padding:16px;display:none}
 .filterBar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
 .filterBar select,.filterBar input{padding:8px;background:#0e1426;border:1px solid #0f3460;color:#eee;font-size:13px;border-radius:4px}
 .filterBar select{flex:1;min-width:100px}
@@ -92,17 +93,18 @@ body{font-family:system-ui,sans-serif;background:#0b1220;color:#e8e8e8;height:10
 
   <div id="alarmPage" class="hidden">
     <div class="filterBar">
-      <select id="fltType" onchange="loadAlarms()">
+      <select id="fltType" onchange="loadAlarms(true)">
         <option value="">全部类型</option>
         <option value="fence">围栏报警</option>
         <option value="pose">姿态报警</option>
       </select>
-      <select id="fltCh" onchange="loadAlarms()"><option value="">全部通道</option></select>
+      <select id="fltCh" onchange="loadAlarms(true)"><option value="">全部通道</option></select>
       <input type="datetime-local" id="fltStart">
       <input type="datetime-local" id="fltEnd">
-      <button onclick="loadAlarms()" style="padding:8px 12px;background:#e94560;border:none;color:#fff;border-radius:4px">查询</button>
+      <button onclick="loadAlarms(true)" style="padding:8px 12px;background:#e94560;border:none;color:#fff;border-radius:4px">查询</button>
     </div>
     <div id="alarmList"></div>
+    <div id="alarmMore">下拉加载更多</div>
   </div>
 </div>
 
@@ -120,6 +122,11 @@ body{font-family:system-ui,sans-serif;background:#0b1220;color:#e8e8e8;height:10
 let channels = [];
 let curChannel = 0;
 let curType = '';
+let alarmItems = [];
+let alarmPage = 0;
+const alarmPageSize = 20;
+let alarmLoading = false;
+let alarmHasMore = false;
 let videoTimer = null;
 let videoBusy = false;
 let videoUrl = '';
@@ -218,7 +225,7 @@ function switchTab(tab) {
   document.getElementById('tabAlarm').classList.toggle('active', tab==='alarm');
   document.getElementById('videoPage').classList.toggle('hidden', tab!=='video');
   document.getElementById('alarmPage').classList.toggle('hidden', tab!=='alarm');
-  if (tab === 'alarm') loadAlarms();
+  if (tab === 'alarm') loadAlarms(true);
 }
 
 function poseColor(a) {
@@ -226,38 +233,52 @@ function poseColor(a) {
   return m[a] || '#8892a8';
 }
 
-async function loadAlarms() {
+function renderAlarmPage() {
+  const start = alarmPage * alarmPageSize;
+  const page = alarmItems.slice(start, start + alarmPageSize);
+  alarmPage++;
+  alarmHasMore = start + alarmPageSize < alarmItems.length;
+  const list = document.getElementById('alarmList');
+  list.insertAdjacentHTML('beforeend', page.map(a => `
+    <div class="alarmItem" style="border-left-color:${a.color}" onclick="showDetail('${a.img}','${a.ts}','${a.label}','${a.detail}',${a.conf},${a.ch})">
+      <img src="/${a.img}" loading="lazy">
+      <div class="info">
+        <div class="time">${a.ts} · CH${a.ch}</div>
+        <div class="type" style="color:${a.color}">${a.label}</div>
+        <div class="meta">${a.detail}</div>
+        <div class="conf">${(a.conf*100).toFixed(0)}%</div>
+      </div>
+    </div>`).join(''));
+  document.getElementById('alarmMore').style.display = alarmHasMore ? 'block' : 'none';
+}
+async function loadAlarms(reset = false) {
+  if (alarmLoading) return;
+  if (!reset && alarmItems.length && alarmHasMore) { renderAlarmPage(); return; }
+  alarmLoading = true;
+  alarmItems = []; alarmPage = 0; alarmHasMore = true;
+  document.getElementById('alarmList').innerHTML = '';
   const type = document.getElementById('fltType').value;
   const ch = document.getElementById('fltCh').value;
   const st = document.getElementById('fltStart').value.replace('T',' ');
   const en = document.getElementById('fltEnd').value.replace('T',' ');
   const qs = new URLSearchParams();
+  qs.set('limit', '100');
   if (ch) qs.set('channel', ch);
   if (st) qs.set('start', st + ':00');
   if (en) qs.set('end', en + ':59');
   try {
-    const [fr, pr] = await Promise.all([
-      fetch('/api/alarms' + (qs ? '?' + qs : '')),
-      fetch('/api/pose/alarms' + (qs ? '?' + qs : ''))
-    ]);
+    const [fr, pr] = await Promise.all([fetch('/api/alarms?' + qs), fetch('/api/pose/alarms?' + qs)]);
     const fd = await fr.json(), pd = await pr.json();
     const fence = (fd.items||[]).map(a => ({t:'围栏', ts:a.timestamp, ch:a.channel, label:a.fence_name, detail:a.class_name, conf:a.confidence, img:a.screenshot, color:'#27ae60'}));
     const pose = (pd.items||[]).map(a => ({t:a.action, ts:a.timestamp, ch:a.channel, label:a.action, detail:'姿态', conf:a.score, img:a.screenshot, color:poseColor(a.action)}));
-    let items = type==='fence' ? fence : type==='pose' ? pose : [...fence, ...pose];
-    items.sort((a,b) => b.ts.localeCompare(a.ts));
-    const list = document.getElementById('alarmList');
-    if (!items.length) { list.innerHTML = '<div class="empty-tip">暂无报警记录</div>'; return; }
-    list.innerHTML = items.slice(0,100).map(a => `
-      <div class="alarmItem" style="border-left-color:${a.color}" onclick="showDetail('${a.img}','${a.ts}','${a.label}','${a.detail}',${a.conf},${a.ch})">
-        <img src="/${a.img}" loading="lazy">
-        <div class="info">
-          <div class="time">${a.ts} · CH${a.ch}</div>
-          <div class="type" style="color:${a.color}">${a.label}</div>
-          <div class="meta">${a.detail}</div>
-          <div class="conf">${(a.conf*100).toFixed(0)}%</div>
-        </div>
-      </div>`).join('');
+    alarmItems = type==='fence' ? fence : type==='pose' ? pose : [...fence, ...pose];
+    alarmItems.sort((a,b) => b.ts.localeCompare(a.ts));
+    if (!alarmItems.length) {
+      document.getElementById('alarmList').innerHTML = '<div class="empty-tip">暂无报警记录</div>';
+      document.getElementById('alarmMore').style.display = 'none';
+    } else renderAlarmPage();
   } catch (e) {}
+  alarmLoading = false;
 }
 
 function showDetail(img, ts, label, detail, conf, ch) {
@@ -268,9 +289,16 @@ function showDetail(img, ts, label, detail, conf, ch) {
 }
 function closeDetail() { document.getElementById('detailModal').classList.remove('show'); }
 
-// 每10秒刷新报警
+document.getElementById('alarmPage').addEventListener('scroll', () => {
+  const el = document.getElementById('alarmPage');
+  if (el.scrollTop + el.clientHeight >= el.scrollHeight - 80 && alarmHasMore && !alarmLoading) {
+    loadAlarms(false);
+  }
+});
+
+// 每10秒刷新报警列表(回到第一页)
 setInterval(() => {
-  if (!document.getElementById('alarmPage').classList.contains('hidden')) loadAlarms();
+  if (!document.getElementById('alarmPage').classList.contains('hidden')) loadAlarms(true);
 }, 10000);
 </script>
 </body>