|
|
@@ -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>
|