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:
@@ -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;
|
bildCache.set(key, neu || null);
|
||||||
setUrl(neu);
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user