Web: Thumbnails cachen (kein Neuladen) + feste Zeilenhoehe

Artikelbilder werden modulweit gecacht (Produkt-ID+Version -> Objekt-URL, auch 'kein Bild'); beim erneuten Oeffnen einer Liste werden sie nicht mehr jedes Mal neu geholt. Der Platzhalter (.thumb-fallback) hat jetzt dieselbe feste Groesse (34x34) wie das Bild, damit die Zeilenhoehe mit/ohne Bild gleich ist und beim Nachladen nicht mehr springt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-27 19:32:59 +02:00
parent 189b1f8e6b
commit c52e88f856
2 changed files with 34 additions and 15 deletions

View File

@@ -12,8 +12,15 @@ import Lightbox from "./Lightbox";
* ``onLoaded`` meldet, ob ein Bild vorliegt so kann die Artikelseite z.B. den * ``onLoaded`` meldet, ob ein Bild vorliegt so kann die Artikelseite z.B. den
* „Entfernen"-Knopf ausblenden, wenn es nichts zu entfernen gibt. * „Entfernen"-Knopf ausblenden, wenn es nichts zu entfernen gibt.
*/ */
// Modulweiter Cache: Produkt-ID+Version → Objekt-URL (oder null = „kein Bild").
// So werden Thumbnails beim erneuten Öffnen einer Liste nicht jedes Mal neu
// geholt. Die Objekt-URLs bleiben absichtlich bestehen (werden nicht widerrufen),
// bis eine neue Version dieselbe ID ablöst.
const bildCache = new Map();
function useBildUrl(productId, version = 0, onLoaded) { function useBildUrl(productId, version = 0, onLoaded) {
const [url, setUrl] = useState(null); const key = `${productId}:${version}`;
const [url, setUrl] = useState(() => (bildCache.has(key) ? bildCache.get(key) : null));
// Über einen Ref, damit ein bei jedem Render neu erzeugtes onLoaded den Effekt // Über einen Ref, damit ein bei jedem Render neu erzeugtes onLoaded den Effekt
// nicht erneut auslöst. // nicht erneut auslöst.
const onLoadedRef = useRef(onLoaded); const onLoadedRef = useRef(onLoaded);
@@ -21,8 +28,13 @@ function useBildUrl(productId, version = 0, onLoaded) {
useEffect(() => { useEffect(() => {
if (!productId) return undefined; if (!productId) return undefined;
if (bildCache.has(key)) {
const cached = bildCache.get(key);
onLoadedRef.current?.(Boolean(cached));
setUrl(cached);
return undefined;
}
let abgebrochen = false; let abgebrochen = false;
let erzeugt = null;
// ``version`` haengt einen Wert an die Adresse, damit der Browser nach dem // ``version`` haengt einen Wert an die Adresse, damit der Browser nach dem
// Speichern nicht seine zwischengespeicherte Fassung ausliefert. // Speichern nicht seine zwischengespeicherte Fassung ausliefert.
@@ -32,24 +44,26 @@ function useBildUrl(productId, version = 0, onLoaded) {
if (neu) URL.revokeObjectURL(neu); if (neu) URL.revokeObjectURL(neu);
return; return;
} }
onLoadedRef.current?.(Boolean(neu)); // Alte Version derselben ID freigeben, dann neu cachen.
if (!neu) { const alt = `${productId}:`;
setUrl(null); for (const k of bildCache.keys()) {
return; if (k !== key && k.startsWith(alt)) {
const u = bildCache.get(k);
if (u) URL.revokeObjectURL(u);
bildCache.delete(k);
} }
erzeugt = neu; }
setUrl(neu); bildCache.set(key, neu || null);
onLoadedRef.current?.(Boolean(neu));
setUrl(neu || null);
}) })
.catch(() => { .catch(() => {
if (!abgebrochen) onLoadedRef.current?.(false); if (!abgebrochen) onLoadedRef.current?.(false);
}); });
return () => { // Objekt-URL NICHT widerrufen sie bleibt im Cache für den nächsten Mount.
abgebrochen = true; return () => { abgebrochen = true; };
if (erzeugt) URL.revokeObjectURL(erzeugt); }, [productId, version, key]);
setUrl(null);
};
}, [productId, version]);
return url; return url;
} }

View File

@@ -400,7 +400,12 @@ td select { width: auto; min-width: 0; max-width: 100%; }
width: 34px; height: 34px; object-fit: cover; width: 34px; height: 34px; object-fit: cover;
border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--surface-2); border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--surface-2);
} }
.thumb-fallback { display: grid; place-items: center; color: var(--muted); } /* Gleiche Größe wie das echte Bild, damit die Zeilenhöhe mit und ohne Bild
identisch ist und beim Nachladen nicht springt. */
.thumb-fallback {
width: 34px; height: 34px; display: grid; place-items: center; color: var(--muted);
border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--surface-2);
}
/* ---------- Badges / pills ---------- */ /* ---------- Badges / pills ---------- */
.badge { .badge {