Browse Source

fix(frontend): 视频分析事件面板与左右面板等高并改为分页显示

- .video-insight-row 由 align-items: start 改为 stretch,状态总览、
  视频分析事件、通行与操作记录三个面板等高;三个面板改为纵向弹性布局,
  状态网格撑满、事件列表内部滚动、记录表格内部滚动
- 视频分析事件每页 5 条,底部新增分页条(共 N 条 · 第 x/y 页 + 上一页/页码/下一页),
  模块切换、重置和重新加载数据时回到第 1 页
康宇 3 tuần trước cách đây
mục cha
commit
045becc161
3 tập tin đã thay đổi với 119 bổ sung và 2 xóa
  1. 48 1
      frontend/api.js
  2. 1 0
      frontend/index.html
  3. 70 1
      frontend/styles.css

+ 48 - 1
frontend/api.js

@@ -2591,6 +2591,8 @@
       channels: [],
       status: {},
       events: {},
+      eventPage: 1,
+      eventPageSize: 5,
       snapshots: {},
       tables: {},
       filters: { quick: "today", start: "", end: "", type: "", scope: "" }
@@ -2934,6 +2936,7 @@
     ]);
     view.status[viewKey] = normalizeVideoStatus(status);
     view.events[viewKey] = (events || []).map(normalizeVideoEvent);
+    view.eventPage = 1;
     view.snapshots[viewKey] = {
       items: (snapshots?.items || []).map(normalizeVideoSnapshot),
       total: Number(snapshots?.total || 0)
@@ -3283,6 +3286,39 @@
     }
   }
 
+  function renderVideoEventPagination(total) {
+    const footer = document.querySelector("#videoEventPagination");
+    if (!footer) return;
+    const view = videoPlatformState();
+    const pageCount = Math.max(1, Math.ceil(total / view.eventPageSize));
+    if (!total) {
+      footer.innerHTML = "";
+      return;
+    }
+    const current = Math.min(Math.max(view.eventPage, 1), pageCount);
+    view.eventPage = current;
+    const pageButtons = Array.from({ length: pageCount }, (_, index) => {
+      const page = index + 1;
+      return `<button class="${page === current ? "active" : ""}" type="button" data-video-event-page="${page}">${page}</button>`;
+    }).join("");
+    footer.innerHTML = `
+      <span>共 ${formatNumber(total)} 条 · 第 ${current}/${pageCount} 页</span>
+      <div>
+        <button type="button" data-video-event-page="${current - 1}" ${current <= 1 ? "disabled" : ""}>上一页</button>
+        ${pageButtons}
+        <button type="button" data-video-event-page="${current + 1}" ${current >= pageCount ? "disabled" : ""}>下一页</button>
+      </div>
+    `;
+    footer.querySelectorAll("[data-video-event-page]").forEach((button) => {
+      button.addEventListener("click", () => {
+        const page = Number(button.dataset.videoEventPage);
+        if (!Number.isFinite(page) || page < 1 || page > pageCount || page === current) return;
+        videoPlatformState().eventPage = page;
+        renderVideoEvents();
+      });
+    });
+  }
+
   function renderVideoEvents() {
     const list = document.querySelector("#videoEventList");
     const count = document.querySelector("#videoEventCount");
@@ -3290,6 +3326,7 @@
     const view = videoPlatformState();
     const events = view.events[state.videoModule] || [];
     if (count) count.textContent = `${formatNumber(events.length)} 条`;
+    renderVideoEventPagination(events.length);
     if (!events.length) {
       list.innerHTML = videoEmptyState(
         "shield-alert",
@@ -3298,7 +3335,15 @@
       window.injectIcons?.(list);
       return;
     }
-    list.innerHTML = events
+    const pageCount = Math.max(1, Math.ceil(events.length / view.eventPageSize));
+    const current = Math.min(Math.max(view.eventPage, 1), pageCount);
+    view.eventPage = current;
+    const pageEvents = events.slice(
+      (current - 1) * view.eventPageSize,
+      current * view.eventPageSize
+    );
+    list.scrollTop = 0;
+    list.innerHTML = pageEvents
       .map(
         (item) => `
           <div class="video-event-row ${escapeHtml(item.tone)}">
@@ -3498,6 +3543,7 @@
       button.addEventListener("click", async () => {
         state.videoModule = button.dataset.videoModule || "door";
         state.videoRecordTab = 0;
+        videoPlatformState().eventPage = 1;
         state.videoSelectedChannel = videoPlatformChannels()[0]?.id || state.videoSelectedChannel;
         renderVideoPlatform();
         await ensureVideoModuleLoaded(state.videoModule);
@@ -3556,6 +3602,7 @@
       state.videoSearch = "";
       if (search) search.value = "";
       state.videoRecordTab = 0;
+      view.eventPage = 1;
       view.filters = { quick: "today", start: "", end: "", type: "", scope: "" };
       applyVideoDefaultRange({ reset: true });
       await searchVideoPlatform({ silent: true, fromInputs: false });

+ 1 - 0
frontend/index.html

@@ -896,6 +896,7 @@
                   <span class="subtle-badge" id="videoEventCount">-- 条</span>
                 </div>
                 <div class="video-event-list" id="videoEventList"></div>
+                <div class="video-event-pagination" id="videoEventPagination"></div>
               </article>
 
               <article class="panel video-records-panel">

+ 70 - 1
frontend/styles.css

@@ -4201,12 +4201,81 @@ body.api-loading .content-area {
 .video-insight-row {
   display: grid;
   grid-template-columns: repeat(3, minmax(0, 1fr));
-  align-items: start;
+  align-items: stretch;
   gap: 14px;
   margin-top: 22px;
   margin-bottom: 14px;
 }
 
+/* 状态总览 / 视频分析事件 / 通行与操作记录 三个面板等高 */
+.video-status-panel,
+.video-event-panel,
+.video-records-panel {
+  display: flex;
+  flex-direction: column;
+}
+
+.video-status-grid {
+  flex: 1;
+  align-content: stretch;
+}
+
+.video-event-list {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+  align-content: start;
+}
+
+.video-event-pagination {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+  margin-top: 10px;
+  padding-top: 9px;
+  color: var(--muted);
+  font-size: 10px;
+  border-top: 1px solid #edf1f4;
+}
+
+.video-event-pagination > div {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 3px;
+}
+
+.video-event-pagination button {
+  display: grid;
+  min-width: 24px;
+  height: 24px;
+  padding: 0 6px;
+  color: #627483;
+  font-size: 10px;
+  cursor: pointer;
+  background: #fff;
+  border: 1px solid var(--line);
+  border-radius: 5px;
+  place-items: center;
+}
+
+.video-event-pagination button.active {
+  color: #fff;
+  background: var(--purple);
+  border-color: var(--purple);
+}
+
+.video-event-pagination button:disabled {
+  cursor: not-allowed;
+  opacity: 0.5;
+}
+
+.video-record-table {
+  flex: 1;
+  min-height: 0;
+}
+
 .video-bottom-layout {
   display: grid;
   grid-template-columns: minmax(0, 1fr);