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
* „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) {
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
// nicht erneut auslöst.
const onLoadedRef = useRef(onLoaded);
@@ -21,8 +28,13 @@ function useBildUrl(productId, version = 0, onLoaded) {
useEffect(() => {
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 erzeugt = null;
// ``version`` haengt einen Wert an die Adresse, damit der Browser nach dem
// Speichern nicht seine zwischengespeicherte Fassung ausliefert.
@@ -32,24 +44,26 @@ function useBildUrl(productId, version = 0, onLoaded) {
if (neu) URL.revokeObjectURL(neu);
return;
}
onLoadedRef.current?.(Boolean(neu));
if (!neu) {
setUrl(null);
return;
// Alte Version derselben ID freigeben, dann neu cachen.
const alt = `${productId}:`;
for (const k of bildCache.keys()) {
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(() => {
if (!abgebrochen) onLoadedRef.current?.(false);
});
return () => {
abgebrochen = true;
if (erzeugt) URL.revokeObjectURL(erzeugt);
setUrl(null);
};
}, [productId, version]);
// Objekt-URL NICHT widerrufen sie bleibt im Cache für den nächsten Mount.
return () => { abgebrochen = true; };
}, [productId, version, key]);
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;
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 ---------- */
.badge {