diff --git a/web/src/components/Cover.tsx b/web/src/components/Cover.tsx new file mode 100644 index 0000000..8406bf8 --- /dev/null +++ b/web/src/components/Cover.tsx @@ -0,0 +1,40 @@ +import { useEffect, useState } from "react"; +import type { Book } from "../api/types"; +import { fetchObjectUrl } from "../lib/authImage"; +import { btn } from "./ui"; + +export function useAuthedImage(url: string | undefined): { src: string; failed: boolean; retry: () => void } { + const [state, setState] = useState<{ key: string; src: string; failed: boolean }>({ key: "", src: "", failed: false }); + const [nonce, setNonce] = useState(0); + const key = (url ?? "") + "#" + nonce; + + useEffect(() => { + if (!url) return; + let dead = false; + const k = url + "#" + nonce; + setState({ key: k, src: "", failed: false }); + fetchObjectUrl(url) + .then((src) => !dead && setState({ key: k, src, failed: false })) + .catch(() => !dead && setState({ key: k, src: "", failed: true })); + return () => { + dead = true; + }; + }, [url, nonce]); + + const same = state.key === key; + return { src: same ? state.src : "", failed: same && state.failed, retry: () => setNonce((n) => n + 1) }; +} + +export function Cover({ book, className }: { book: Book; className?: string }) { + const { src, failed, retry } = useAuthedImage(book.cover_url); + if (failed) + return ( +
+ +
+ ); + if (!src) return
; + return ; +} diff --git a/web/src/lib/authImage.ts b/web/src/lib/authImage.ts new file mode 100644 index 0000000..758c95b --- /dev/null +++ b/web/src/lib/authImage.ts @@ -0,0 +1,30 @@ +import { apiRaw } from "../api/client"; + +const MAX = 600; +const cache = new Map>(); // 插入序 ≈ LRU(命中时重插) + +// 带鉴权取图 → objectURL。后端 Bearer-only,/