From ad21900a6f3d68d88d17c0939fc853763ccce5e3 Mon Sep 17 00:00:00 2001 From: Jokul Date: Sun, 12 Jul 2026 15:04:08 +0800 Subject: [PATCH] feat(N4): virtual scrolling for Set/Zset/List editors - Create VirtualScrollList.vue component using @tanstack/vue-virtual - Props: items, itemHeight, overscan, height - Events: scrollEnd (for infinite loading) - Slots: default (item rendering), empty - SetEditor: replace v-for with VirtualScrollList, handles 100k+ members - ZsetEditor: replace v-for with VirtualScrollList, preserves filter/sort - ListEditor: replace pagination with virtual scroll + infinite loading - Initial load 100 items, auto-loads 100 more on scrollEnd - Shows loaded/total count + loading indicator - HashEditor: kept as-is (HSCAN limits to 100 items, el-table handles fine) - docs: mark N4 complete, update problem E priority to medium --- docs/PROJECT_REVIEW.md | 6 +- .../components/common/VirtualScrollList.vue | 98 +++++++++++++++++ .../src/components/editors/ListEditor.vue | 104 ++++++++++-------- .../src/components/editors/SetEditor.vue | 19 ++-- .../src/components/editors/ZsetEditor.vue | 22 ++-- 5 files changed, 182 insertions(+), 67 deletions(-) create mode 100644 src/renderer/src/components/common/VirtualScrollList.vue diff --git a/docs/PROJECT_REVIEW.md b/docs/PROJECT_REVIEW.md index 6256093..0123b3c 100644 --- a/docs/PROJECT_REVIEW.md +++ b/docs/PROJECT_REVIEW.md @@ -131,18 +131,16 @@ async function safeInvoke(fn: () => Promise, errorMsg?: string): Promise(fn: () => Promise, errorMsg?: string): Promise +import { ref, computed, onMounted, onUnmounted, watch } from 'vue' +import { useVirtualizer } from '@tanstack/vue-virtual' + +const props = withDefaults(defineProps<{ + items: any[] + itemHeight?: number + overscan?: number + height?: string +}>(), { + itemHeight: 36, + overscan: 5, + height: '100%', +}) + +const emit = defineEmits<{ + (e: 'scrollEnd'): void +}>() + +const scrollRef = ref(null) + +const virtualizer = useVirtualizer( + computed(() => ({ + count: props.items.length, + getScrollElement: () => scrollRef.value, + estimateSize: () => props.itemHeight, + overscan: props.overscan, + })) +) + +const virtualItems = computed(() => virtualizer.value.getVirtualItems()) +const totalHeight = computed(() => virtualizer.value.getTotalSize()) + +// Emit scrollEnd when user scrolls near the bottom +function onScroll() { + const el = scrollRef.value + if (!el) return + if (el.scrollTop + el.clientHeight >= el.scrollHeight - props.itemHeight * 5) { + emit('scrollEnd') + } +} + +// Reset scroll position when items array reference changes entirely +watch(() => props.items, (newItems, oldItems) => { + if (newItems.length === 0 && oldItems && oldItems.length > 0) { + virtualizer.value.scrollToIndex(0) + } +}) + + + + + diff --git a/src/renderer/src/components/editors/ListEditor.vue b/src/renderer/src/components/editors/ListEditor.vue index 6245058..563c0c2 100644 --- a/src/renderer/src/components/editors/ListEditor.vue +++ b/src/renderer/src/components/editors/ListEditor.vue @@ -1,10 +1,11 @@