From 23f4ac18f4a4858cfb1082fd315116b52cab2173 Mon Sep 17 00:00:00 2001 From: CritasWang Date: Thu, 2 Jul 2026 17:13:15 +0800 Subject: [PATCH] fix: data preview table body overlapping pagination on load The data preview table hardcoded the virtual-scroll viewport height (scroll.y = 480). After #27 added the timestamp-precision info banner above the table, the fixed height no longer matched the actual space left in the flex layout, so the table body overflowed onto the bottom pagination bar. Replace the hardcoded height with a measured one: wrap the table in a flex-1 container and use a ResizeObserver to compute scroll.y from the container's real client height, subtracting the measured header height plus a scrollbar/border reserve. Re-measure after loading/data changes (nextTick) since the column toolbar, info banner and pagination toggle visibility. This adapts automatically to any future elements added above or below the table instead of relying on a magic constant. Also make .ant-card-body a flex column so the wrapper's flex-1 receives the real remaining height, and drop an unused Spin import. --- frontend/src/components/tsfile/DataTable.vue | 62 ++++++++++++++++++-- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/tsfile/DataTable.vue b/frontend/src/components/tsfile/DataTable.vue index 3c2c5f1..337e526 100644 --- a/frontend/src/components/tsfile/DataTable.vue +++ b/frontend/src/components/tsfile/DataTable.vue @@ -25,10 +25,10 @@ import type { DataRow } from "@/api/tsfile/types"; import type { TableColumnType } from "antdv-next"; -import { computed, ref, watch } from "vue"; +import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"; import { useI18n } from "vue-i18n"; -import { Alert, Button, Card, Input, Pagination, Select, Spin, Table, Tooltip } from "antdv-next"; +import { Alert, Button, Card, Input, Pagination, Select, Table, Tooltip } from "antdv-next"; import { DownloadOutlined } from "@antdv-next/icons"; import { formatTimestamp } from "@/utils/timestamp"; @@ -185,12 +185,53 @@ const columns = computed(() => { return cols; }); -// 表格横向滚动宽度(用于虚拟滚动,需为数字) +// 表格横向滚动宽度(固定列 + 当前可见字段列,撑出横向滚动条) const scrollX = computed(() => { const fixedWidth = 210 + 180 + props.tagColumns.length * 120; return fixedWidth + visibleFieldColumns.value.length * 120; }); +// 表格纵向可视高度:用 ResizeObserver 实测表格外层容器的可用高度, +// 避免用写死常量手算上方元素(列控制条 / 精度说明 Alert / 分页)的高度—— +// 上方任意增删元素时,虚拟滚动的 scroll.y 都能自适应,不再错位。 +const tableWrapper = ref(null); +const tableBodyHeight = ref(400); +let resizeObserver: ResizeObserver | null = null; +// 表头 DOM 未就绪时的兜底预留高度(表头 + 横向滚动条 + 边框) +const HEADER_RESERVE_FALLBACK = 64; +// 横向滚动条 + 底部边框额外预留,避免 body 覆盖到下方分页 +const SCROLLBAR_RESERVE = 16; + +function measureTableHeight() { + const el = tableWrapper.value; + if (!el) return; + // 优先实测真实表头高度(适配 size / 多行表头 / 字体变化),拿不到则用兜底值 + const headerEl = el.querySelector(".ant-table-header, thead"); + const reserve = (headerEl?.offsetHeight || HEADER_RESERVE_FALLBACK) + SCROLLBAR_RESERVE; + const h = el.clientHeight - reserve; + tableBodyHeight.value = Math.max(120, Math.floor(h)); +} + +onMounted(() => { + measureTableHeight(); + if (typeof ResizeObserver !== "undefined" && tableWrapper.value) { + resizeObserver = new ResizeObserver(() => measureTableHeight()); + resizeObserver.observe(tableWrapper.value); + } +}); + +onBeforeUnmount(() => { + resizeObserver?.disconnect(); + resizeObserver = null; +}); + +// loading 结束 / 数据变化后,表头与上下方控制条、分页的显隐会改变, +// 等 DOM 更新后重新实测表头高度,确保 body 高度精确、不覆盖分页。 +watch( + () => [props.loading, props.data.length, props.total] as const, + () => nextTick(measureTableHeight), +); + // 表格数据(扁平化)—— 仅展开当前可见列,避免每行生成数千个属性 const tableData = computed(() => { const visibleMeasurements = [...props.tagColumns, ...visibleFieldColumns.value]; @@ -356,14 +397,14 @@ function formatValue(value: unknown): number | string { banner /> - + +
+
@@ -424,6 +466,14 @@ function formatValue(value: unknown): number | string {