fix: improve material preview loading

This commit is contained in:
Your Name 2026-06-01 13:56:21 +08:00
parent 1c8a709670
commit 291eb9b11c
5 changed files with 81 additions and 12 deletions

View File

@ -34,6 +34,7 @@ type ExternalImageRecord = {
label?: string;
fileName?: string;
fileSize?: number;
previewUrl?: string;
downloadUrl?: string;
};
@ -81,6 +82,7 @@ type MaterialSearchImage = {
label: string;
fileName: string;
imageUrl: string;
previewUrl: string | null;
assetId: string | null;
mimeType: string;
group: "fixed" | "other";
@ -611,6 +613,7 @@ function mapExternalImage(params: {
label: String(params.image.label || "").trim() || `图片${params.sortOrder + 1}`,
fileName: filename,
imageUrl: downloadUrl,
previewUrl: String(params.image.previewUrl || "").trim() || null,
assetId: null,
mimeType: inferMimeTypeFromFilename(filename),
group: params.group,

View File

@ -18,6 +18,9 @@ type AgentPreviewImageProps = Omit<
previewWidth?: number;
previewQuality?: number;
previewFormat?: "webp" | "avif" | "jpeg" | "png";
delivery?: "display" | "proxy";
fallbackSrc?: string | null;
fallbackToOriginal?: boolean;
rootMargin?: string;
eager?: boolean;
raw?: boolean;
@ -31,6 +34,7 @@ function buildPreviewUrl(params: {
width: number;
quality: number;
format: "webp" | "avif" | "jpeg" | "png";
delivery?: "display" | "proxy";
raw?: boolean;
}) {
if (params.raw) return params.src;
@ -39,6 +43,7 @@ function buildPreviewUrl(params: {
width: params.width,
quality: params.quality,
format: params.format,
delivery: params.delivery,
}) || params.src
);
}
@ -48,6 +53,9 @@ function AgentPreviewImageBase({
previewWidth = 480,
previewQuality = 78,
previewFormat = "webp",
delivery = "display",
fallbackSrc,
fallbackToOriginal = true,
rootMargin = "640px 0px",
eager = false,
raw = false,
@ -58,15 +66,22 @@ function AgentPreviewImageBase({
const imageRef = useRef<HTMLImageElement | null>(null);
const [shouldLoad, setShouldLoad] = useState(eager);
const [useOriginal, setUseOriginal] = useState(false);
const [useFallbackSource, setUseFallbackSource] = useState(false);
const normalizedSrc = String(src || "").trim();
const normalizedFallbackSrc = String(fallbackSrc || "").trim();
const currentSrc =
useFallbackSource && normalizedFallbackSrc
? normalizedFallbackSrc
: normalizedSrc;
useEffect(() => {
setUseOriginal(false);
setUseFallbackSource(false);
setShouldLoad(eager);
}, [eager, normalizedSrc]);
}, [eager, normalizedFallbackSrc, normalizedSrc]);
useEffect(() => {
if (eager || shouldLoad || !normalizedSrc) return;
if (eager || shouldLoad || !currentSrc) return;
const image = imageRef.current;
if (!image) return;
@ -86,20 +101,22 @@ function AgentPreviewImageBase({
);
observer.observe(image);
return () => observer.disconnect();
}, [eager, normalizedSrc, rootMargin, shouldLoad]);
}, [currentSrc, eager, rootMargin, shouldLoad]);
const displaySrc = useMemo(() => {
if (!normalizedSrc || !shouldLoad) return EMPTY_PIXEL;
if (useOriginal) return normalizedSrc;
if (!currentSrc || !shouldLoad) return EMPTY_PIXEL;
if (useOriginal) return currentSrc;
return buildPreviewUrl({
src: normalizedSrc,
src: currentSrc,
width: previewWidth,
quality: previewQuality,
format: previewFormat,
delivery,
raw,
});
}, [
normalizedSrc,
currentSrc,
delivery,
previewFormat,
previewQuality,
previewWidth,
@ -117,10 +134,20 @@ function AgentPreviewImageBase({
loading={eager ? "eager" : "lazy"}
decoding="async"
onError={(event) => {
if (normalizedSrc && !useOriginal && displaySrc !== normalizedSrc) {
if (
fallbackToOriginal &&
currentSrc &&
!useOriginal &&
displaySrc !== currentSrc
) {
setUseOriginal(true);
return;
}
if (!useFallbackSource && normalizedFallbackSrc) {
setUseFallbackSource(true);
setUseOriginal(false);
return;
}
onError?.(event);
}}
onLoad={onLoad}

View File

@ -75,6 +75,17 @@ const MaterialImageCard = memo(function MaterialImageCard({
onToggle: (imageId: string) => void;
}) {
const isPlaceholder = !String(image.imageUrl || "").trim();
const previewUrl = String(image.previewUrl || "").trim();
const displayImageUrl = previewUrl || String(image.imageUrl || "").trim();
const fallbackImageUrl = previewUrl ? String(image.imageUrl || "").trim() : null;
const [imageStatus, setImageStatus] = useState<"loading" | "loaded" | "error">(
isPlaceholder ? "loaded" : "loading"
);
useEffect(() => {
setImageStatus(isPlaceholder ? "loaded" : "loading");
}, [displayImageUrl, fallbackImageUrl, isPlaceholder]);
return (
<button
type="button"
@ -109,16 +120,41 @@ const MaterialImageCard = memo(function MaterialImageCard({
</div>
) : (
<AgentPreviewImage
src={image.imageUrl}
src={displayImageUrl}
fallbackSrc={fallbackImageUrl}
alt={image.label}
className="h-full w-full object-cover"
className={cn(
"h-full w-full object-contain transition-opacity duration-300",
imageStatus === "loaded" ? "opacity-100" : "opacity-0"
)}
delivery="proxy"
eager
fallbackToOriginal={false}
previewWidth={480}
previewQuality={74}
onError={(event) => {
event.currentTarget.style.display = "none";
onError={() => {
setImageStatus("error");
}}
onLoad={() => setImageStatus("loaded")}
/>
)}
{!isPlaceholder && imageStatus !== "loaded" ? (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-[radial-gradient(circle_at_top,rgba(16,185,129,0.16),transparent_58%),linear-gradient(180deg,rgba(255,255,255,0.07),rgba(255,255,255,0.02))] px-4 text-center text-white/70">
{imageStatus === "error" ? (
<ImageIcon className="h-7 w-7 text-white/45" />
) : (
<Loader2 className="h-6 w-6 animate-spin text-emerald-200/80" />
)}
<div className="text-xs font-medium">
{imageStatus === "error" ? "预览暂不可用" : "正在加载预览"}
</div>
<div className="max-w-[180px] text-[11px] leading-5 text-white/45">
{imageStatus === "error"
? "原图地址仍可用于后续生成,可先选中或稍后重试。"
: "已命中素材,正在拉取缩略预览。"}
</div>
</div>
) : null}
<div className="absolute inset-x-0 top-0 flex items-center justify-between px-2 py-2">
<span className="rounded-full bg-black/60 px-2 py-1 text-[11px] text-white">
{isPlaceholder ? "加载中" : image.group === "fixed" ? "固定角度" : "其他图片"}

View File

@ -41,6 +41,7 @@ function normalizeMaterialImage(
const id = sanitizeText(raw.id, 128) || `image-${index + 1}`;
const group = raw.group === "other" ? "other" : "fixed";
const previewUrl = sanitizeText(raw.previewUrl, 2000) || null;
return {
id,
@ -48,6 +49,7 @@ function normalizeMaterialImage(
label: sanitizeText(raw.label, 128) || `图片${index + 1}`,
fileName: sanitizeText(raw.fileName, 255) || `image-${index + 1}`,
imageUrl,
previewUrl,
assetId: sanitizeText(raw.assetId, 128) || null,
mimeType: sanitizeText(raw.mimeType, 128) || null,
appearanceSummary: sanitizeText(raw.appearanceSummary, 255) || null,

View File

@ -272,6 +272,7 @@ export interface AgentWorkflowMaterialImage {
label: string;
fileName: string;
imageUrl: string;
previewUrl?: string | null;
assetId?: string | null;
mimeType?: string | null;
appearanceSummary?: string | null;