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 @@
<template> <template>
<div class="list-table-v2" :style="containerStyle"> <div class="list-table-v2" :style="containerStyle">
<!-- Probe row for measuring actual row height (only when using dynamic height mode) --> <!-- Mini hidden table for measuring row height (only when using dynamic height mode) -->
<div v-if="shouldUseProbeRow" ref="probeRowRef" class="probe-row" aria-hidden="true"> <div v-if="shouldUseProbeRow" ref="miniTableRef" class="mini-table" aria-hidden="true">
<div v-if="probeRowData" class="probe-cell"> <div v-if="miniTableData.length > 0" class="mini-table-inner">
<template v-for="item in prop.columns || []" :key="item.key"> <div v-for="(row, idx) in miniTableData" :key="idx" class="mini-row">
<span class="probe-content"> <div v-for="item in prop.columns || []" :key="item.key" class="mini-cell">
{{ getProbeCellText(probeRowData, item) }} <span class="mini-cell-content">
</span> {{ getProbeCellText(row, item) }}
</template> </span>
</div>
</div>
</div> </div>
</div> </div>
<div class="my-table"> <div ref="myTableRef" class="my-table">
<el-auto-resizer> <el-auto-resizer>
<template #default="{ height, width }"> <template #default="{ height, width }">
<el-table-v2 <el-table-v2
@ -61,13 +63,11 @@ const slots = useSlots();
const { t } = useI18n(); const { t } = useI18n();
const { state } = useStore(); const { state } = useStore();
const table = ref(); const table = ref();
const probeRowRef = ref(); const miniTableRef = ref();
const probeRowData = ref(); const myTableRef = ref(); // Reference to .my-table for ResizeObserver
const miniTableData = ref<any[]>([]);
const estimatedRowHeight = ref<number | undefined>(undefined); const estimatedRowHeight = ref<number | undefined>(undefined);
let miniTableResizeObserver: ResizeObserver | null = null;
// Debounced resize handler for re-measuring probe row
// Use LodashDebounce for proper cancel() method typing
const debouncedResizeHandler = ref<lodash.DebouncedFunc<() => void> | undefined>(undefined);
// Header height constant // Header height constant
@ -194,22 +194,25 @@ const estimatedRowHeightToUse = computed(() => {
return estimatedRowHeight.value; // use probe-measured value 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 // We use TWO frames delay to let el-table-v2 fully settle before measuring
watch( watch(
() => pageData.value?.[0], () => pageData.value,
async (firstRow) => { async (data) => {
// Skip if rowHeight is set (fixed height mode) or already have a value // Skip if rowHeight is set (fixed height mode) or no data
if (!shouldUseProbeRow.value || !firstRow) { if (!shouldUseProbeRow.value || !data || data.length === 0) {
return; return;
} }
probeRowData.value = firstRow; // Use up to 5 rows for mini table
// Wait for probe to render miniTableData.value = data.slice(0, 5);
// Wait for mini table to render
await nextTick(); 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));
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) { if (height && height > 0) {
estimatedRowHeight.value = height; estimatedRowHeight.value = height;
} }
@ -217,45 +220,46 @@ watch(
{ immediate: true } { immediate: true }
); );
// Setup resize listener on mount // Setup ResizeObserver on mount (not window resize - that causes flickering)
onMounted(() => { onMounted(() => {
// Create debounced handler // Create debounced resize handler for mini table width changes
debouncedResizeHandler.value = lodash.debounce(() => { const debouncedMeasure = lodash.debounce(async () => {
// Only re-measure in dynamic height mode if (!shouldUseProbeRow.value || miniTableData.value.length === 0) {
if (shouldUseProbeRow.value) { return;
measureProbeRow();
} }
}, 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 if (myTableRef.value) {
window.addEventListener("resize", debouncedResizeHandler.value); miniTableResizeObserver.observe(myTableRef.value);
}
}); });
// Cleanup on unmount // Cleanup on unmount
onUnmounted(() => { onUnmounted(() => {
if (debouncedResizeHandler.value) { if (miniTableResizeObserver) {
window.removeEventListener("resize", debouncedResizeHandler.value); miniTableResizeObserver.disconnect();
debouncedResizeHandler.value.cancel(); // Cancel any pending debounce calls 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 containerStyle = computed(() => {
const style: Record<string, string> = {}; const style: Record<string, string> = {};
if (prop.height !== undefined) { if (prop.height !== undefined) {
@ -455,26 +459,55 @@ const tableColumns = computed(() => {
position: relative; position: relative;
} }
// Probe row for measuring actual row height // Mini hidden table for measuring row height (flex boxes, not el-table-v2)
.probe-row { .mini-table {
position: absolute; position: absolute;
visibility: hidden; visibility: hidden;
pointer-events: none; pointer-events: none;
left: -9999px; left: -9999px;
top: 0; 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; display: flex;
align-items: center; align-items: center;
padding: v-bind("state.size.tablePad"); padding: v-bind("state.size.tablePad");
border-right: var(--el-table-border); border-right: var(--el-table-border);
border-bottom: var(--el-table-border);
background: v-bind("state.style.tableBg"); background: v-bind("state.style.tableBg");
color: v-bind("state.style.tableColor"); 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; 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; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@ -486,6 +519,7 @@ const tableColumns = computed(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
position: relative; // For mini-table's containing block
} }
.my-table :deep(.el-table-v2) { .my-table :deep(.el-table-v2) {

2
packages/manage/router/index.vue

@ -37,7 +37,7 @@ import { useRouter, useRoute } from "vue-router";
import { Api, NoobHead } from "noob-mengyxu"; import { Api, NoobHead } from "noob-mengyxu";
import md5 from "js-md5"; 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 { VITE_APP_VERSION, VITE_GIT_HASH, NODE_ENV } = import.meta.env;
const { Head, MenuTree, HeadPersonal, Fullscreen, StyleChange, LangChange, SizeChange } = NoobHead; const { Head, MenuTree, HeadPersonal, Fullscreen, StyleChange, LangChange, SizeChange } = NoobHead;

Loading…
Cancel
Save