fix(knowledge): replace IntersectionObserver with scroll event; increase card height
- Swap IntersectionObserver (race-prone, fired immediately on creation) for a passive scroll listener on the grid container — eliminates duplicate page loads caused by observer re-creation after DOM updates - Increase card min-height 100px → 160px so tags + snippet are visible - Increase snippet line-clamp 3 → 4 for more content preview Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted, nextTick, watchEffect } from "vue";
|
||||
import { ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { apiGet, apiPatch } from "@/api/client";
|
||||
import { fmtCompact } from "@/utils/dateFormat";
|
||||
@@ -244,23 +244,16 @@ function formatDate(iso: string): string {
|
||||
|
||||
// ─── Infinite scroll ──────────────────────────────────────────────────────────
|
||||
|
||||
const sentinelEl = ref<HTMLElement | null>(null);
|
||||
const cardGridEl = ref<HTMLElement | null>(null);
|
||||
let scrollObserver: IntersectionObserver | null = null;
|
||||
|
||||
function setupScrollObserver() {
|
||||
if (scrollObserver) scrollObserver.disconnect();
|
||||
if (!sentinelEl.value || !cardGridEl.value) return;
|
||||
scrollObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && !loading.value && items.value.length < total.value) {
|
||||
page.value++;
|
||||
fetchItems();
|
||||
}
|
||||
},
|
||||
{ root: cardGridEl.value, rootMargin: "200px" }
|
||||
);
|
||||
scrollObserver.observe(sentinelEl.value);
|
||||
function onGridScroll() {
|
||||
const el = cardGridEl.value;
|
||||
if (!el || loading.value || items.value.length >= total.value) return;
|
||||
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 300;
|
||||
if (nearBottom) {
|
||||
page.value++;
|
||||
fetchItems();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Lifecycle ────────────────────────────────────────────────────────────────
|
||||
@@ -269,16 +262,10 @@ onMounted(() => {
|
||||
fetchItems(true);
|
||||
fetchTags();
|
||||
fetchTodayBar();
|
||||
nextTick(() => setupScrollObserver());
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (searchDebounce) clearTimeout(searchDebounce);
|
||||
scrollObserver?.disconnect();
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (sentinelEl.value && cardGridEl.value) setupScrollObserver();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -380,8 +367,8 @@ watchEffect(() => {
|
||||
<p v-else class="empty-hint">Start by creating a note, saving a person or place, or making a list.</p>
|
||||
</div>
|
||||
|
||||
<!-- Card grid (sentinel lives inside so IntersectionObserver root works) -->
|
||||
<div v-else ref="cardGridEl" class="card-grid">
|
||||
<!-- Card grid -->
|
||||
<div v-else ref="cardGridEl" class="card-grid" @scroll.passive="onGridScroll">
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
@@ -446,10 +433,8 @@ watchEffect(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sentinel after all cards — IntersectionObserver triggers next page load -->
|
||||
<div ref="sentinelEl" class="scroll-sentinel">
|
||||
<span v-if="loading && items.length > 0" class="sentinel-loading">Loading…</span>
|
||||
</div>
|
||||
<!-- Loading indicator when fetching additional pages -->
|
||||
<div v-if="loading && items.length > 0" class="load-more-indicator">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -730,7 +715,7 @@ watchEffect(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 100px;
|
||||
min-height: 160px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.k-card:hover {
|
||||
@@ -777,7 +762,7 @@ watchEffect(() => {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-muted);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.45;
|
||||
@@ -877,15 +862,11 @@ watchEffect(() => {
|
||||
}
|
||||
.empty-hint { font-size: 0.85rem; opacity: 0.7; }
|
||||
|
||||
/* ── Infinite scroll sentinel ────────────────────────────── */
|
||||
.scroll-sentinel {
|
||||
grid-column: 1 / -1; /* span all columns so it's not a card-sized slot */
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.sentinel-loading {
|
||||
/* ── Load more indicator ─────────────────────────────────── */
|
||||
.load-more-indicator {
|
||||
grid-column: 1 / -1;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user