fix: improve material preview loading
This commit is contained in:
parent
1c8a709670
commit
291eb9b11c
@ -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,
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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" ? "固定角度" : "其他图片"}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user