Browse Source

refactor: replace window resize with ResizeObserver-based mini table for row height

- Replace window resize listener with ResizeObserver on .my-table container
- Mini table now uses 3-5 sample rows with pure flex layout
- No flickering during resize/scroll since ResizeObserver only fires on actual size changes
- Remove window resize event listener entirely
- Update DEV_MODE_TS timestamp

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dev
hechang27-sprt 3 months ago
parent
commit
242f376476
  1. 148
      packages/base/data/list-table-v2.vue
  2. 2
      packages/manage/router/index.vue

148
packages/base/data/list-table-v2.vue

@ -1,17 +1,19 @@ @@ -1,17 +1,19 @@
<template>
<div class="list-table-v2" :style="containerStyle">
<!-- Probe row for measuring actual row height (only when using dynamic height mode) -->
<div v-if="shouldUseProbeRow" ref="probeRowRef" class="probe-row" aria-hidden="true">
<div v-if="probeRowData" class="probe-cell">
<template v-for="item in prop.columns || []" :key="item.key">
<span class="probe-content">
{{ getProbeCellText(probeRowData, item) }}
</span>
</template>
<!-- Mini hidden table for measuring row height (only when using dynamic height mode) -->
<div v-if="shouldUseProbeRow" ref="miniTableRef" class="mini-table" aria-hidden="true">
<div v-if="miniTableData.length > 0" class="mini-table-inner">
<div v-for="(row, idx) in miniTableData" :key="idx" class="mini-row">
<div v-for="item in prop.columns || []" :key="item.key" class="mini-cell">
<span class="mini-cell-content">
{{ getProbeCellText(row, item) }}
</span>
</div>
</div>
</div>
</div>
<div class="my-table">
<div ref="myTableRef" class="my-table">
<el-auto-resizer>
<template #default="{ height, width }">
<el-table-v2
@ -61,13 +63,11 @@ const slots = useSlots(); @@ -61,13 +63,11 @@ const slots = useSlots();
const { t } = useI18n();
const { state } = useStore();
const table = ref();
const probeRowRef = ref();
const probeRowData = ref();
const miniTableRef = ref();
const myTableRef = ref(); // Reference to .my-table for ResizeObserver
const miniTableData = ref<any[]>([]);
const estimatedRowHeight = ref<number | undefined>(undefined);
// Debounced resize handler for re-measuring probe row
// Use LodashDebounce for proper cancel() method typing
const debouncedResizeHandler = ref<lodash.DebouncedFunc<() => void> | undefined>(undefined);
let miniTableResizeObserver: ResizeObserver | null = null;
// Header height constant
@ -194,22 +194,25 @@ const estimatedRowHeightToUse = computed(() => { @@ -194,22 +194,25 @@ const estimatedRowHeightToUse = computed(() => {
return estimatedRowHeight.value; // use probe-measured value
});
// Measure probe row height when data changes (only when using probe row)
// Measure mini table row height when data changes (only when using probe row)
// We use TWO frames delay to let el-table-v2 fully settle before measuring
watch(
() => pageData.value?.[0],
async (firstRow) => {
// Skip if rowHeight is set (fixed height mode) or already have a value
if (!shouldUseProbeRow.value || !firstRow) {
() => pageData.value,
async (data) => {
// Skip if rowHeight is set (fixed height mode) or no data
if (!shouldUseProbeRow.value || !data || data.length === 0) {
return;
}
probeRowData.value = firstRow;
// Wait for probe to render
// Use up to 5 rows for mini table
miniTableData.value = data.slice(0, 5);
// Wait for mini table to render
await nextTick();
// Wait TWO frames for el-table-v2 to fully settle its layout
// Wait TWO frames for layout to settle
await new Promise((resolve) => requestAnimationFrame(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
const height = probeRowRef.value?.offsetHeight;
// Measure first row height
const firstRow = miniTableRef.value?.querySelector(".mini-row");
const height = firstRow?.offsetHeight;
if (height && height > 0) {
estimatedRowHeight.value = height;
}
@ -217,45 +220,46 @@ watch( @@ -217,45 +220,46 @@ watch(
{ immediate: true }
);
// Setup resize listener on mount
// Setup ResizeObserver on mount (not window resize - that causes flickering)
onMounted(() => {
// Create debounced handler
debouncedResizeHandler.value = lodash.debounce(() => {
// Only re-measure in dynamic height mode
if (shouldUseProbeRow.value) {
measureProbeRow();
// Create debounced resize handler for mini table width changes
const debouncedMeasure = lodash.debounce(async () => {
if (!shouldUseProbeRow.value || miniTableData.value.length === 0) {
return;
}
}, 250); // 250ms debounce delay
// Clear previous measurement so el-table-v2 recalculates
estimatedRowHeight.value = undefined;
await nextTick();
// Wait TWO frames for layout to settle
await new Promise((resolve) => requestAnimationFrame(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
// Measure first row height
const firstRow = miniTableRef.value?.querySelector(".mini-row");
const height = firstRow?.offsetHeight;
if (height && height > 0) {
estimatedRowHeight.value = height;
}
}, 100);
// Use ResizeObserver on .my-table to detect width changes
// This is better than window resize because it only fires when OUR container changes
miniTableResizeObserver = new ResizeObserver(() => {
debouncedMeasure();
});
// Attach window resize listener
window.addEventListener("resize", debouncedResizeHandler.value);
if (myTableRef.value) {
miniTableResizeObserver.observe(myTableRef.value);
}
});
// Cleanup on unmount
onUnmounted(() => {
if (debouncedResizeHandler.value) {
window.removeEventListener("resize", debouncedResizeHandler.value);
debouncedResizeHandler.value.cancel(); // Cancel any pending debounce calls
if (miniTableResizeObserver) {
miniTableResizeObserver.disconnect();
miniTableResizeObserver = null;
}
});
// Function to re-measure probe row height (for resize handling)
const measureProbeRow = async () => {
if (!shouldUseProbeRow.value || !probeRowData.value) {
return;
}
// Clear previous measurement so el-table-v2 recalculates
estimatedRowHeight.value = undefined;
await nextTick();
// Wait TWO frames for el-table-v2 to fully settle its layout
await new Promise((resolve) => requestAnimationFrame(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
const height = probeRowRef.value?.offsetHeight;
if (height && height > 0) {
estimatedRowHeight.value = height;
}
};
const containerStyle = computed(() => {
const style: Record<string, string> = {};
if (prop.height !== undefined) {
@ -455,26 +459,55 @@ const tableColumns = computed(() => { @@ -455,26 +459,55 @@ const tableColumns = computed(() => {
position: relative;
}
// Probe row for measuring actual row height
.probe-row {
// Mini hidden table for measuring row height (flex boxes, not el-table-v2)
.mini-table {
position: absolute;
visibility: hidden;
pointer-events: none;
left: -9999px;
top: 0;
width: 100%; // Width responds to parent container
max-width: 100%;
}
.probe-cell {
.mini-table-inner {
display: flex;
flex-direction: column;
width: 100%;
}
.mini-row {
display: flex;
align-items: center;
padding: v-bind("state.size.tablePad");
border-right: var(--el-table-border);
border-bottom: var(--el-table-border);
background: v-bind("state.style.tableBg");
color: v-bind("state.style.tableColor");
box-sizing: border-box;
}
.mini-cell {
flex: 1;
min-width: 120px;
padding-right: 8px;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.probe-content {
// Flex grow columns match the real table's flex distribution
.mini-cell:nth-child(1) { flex: 1; min-width: 120px; }
.mini-cell:nth-child(2) { flex: 1; min-width: 120px; }
.mini-cell:nth-child(3) { flex: 1; min-width: 120px; }
.mini-cell:nth-child(4) { flex: 1; min-width: 120px; }
.mini-cell:nth-child(5) { flex: 1; min-width: 120px; }
.mini-cell:nth-child(6) { flex: 1; min-width: 120px; }
.mini-cell:nth-child(7) { flex: 1; min-width: 120px; }
.mini-cell:nth-child(8) { flex: 1; min-width: 120px; }
.mini-cell-content {
overflow: hidden;
text-overflow: ellipsis;
}
@ -486,6 +519,7 @@ const tableColumns = computed(() => { @@ -486,6 +519,7 @@ const tableColumns = computed(() => {
display: flex;
flex-direction: column;
overflow: hidden;
position: relative; // For mini-table's containing block
}
.my-table :deep(.el-table-v2) {

2
packages/manage/router/index.vue

@ -37,7 +37,7 @@ import { useRouter, useRoute } from "vue-router"; @@ -37,7 +37,7 @@ import { useRouter, useRoute } from "vue-router";
import { Api, NoobHead } from "noob-mengyxu";
import md5 from "js-md5";
const DEV_MODE_TS = "2026-03-26T07:25:00.000Z";
const DEV_MODE_TS = "2026-03-26T07:50:00.000Z";
const { VITE_APP_VERSION, VITE_GIT_HASH, NODE_ENV } = import.meta.env;
const { Head, MenuTree, HeadPersonal, Fullscreen, StyleChange, LangChange, SizeChange } = NoobHead;

Loading…
Cancel
Save