fix: harden detail reference stitch loading
This commit is contained in:
parent
4afced3b8d
commit
b40df45daa
@ -33,6 +33,7 @@ type LoadedReferenceImage = {
|
||||
width: number;
|
||||
height: number;
|
||||
scaledHeight: number;
|
||||
revokeObjectUrl?: (() => void) | null;
|
||||
};
|
||||
|
||||
type MergedSnapshot = {
|
||||
@ -46,20 +47,97 @@ function clamp(value: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function loadImage(url: string) {
|
||||
function createImageElement(src: string, useCrossOrigin = true) {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.crossOrigin = "anonymous";
|
||||
if (useCrossOrigin) {
|
||||
image.crossOrigin = "anonymous";
|
||||
}
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = () => reject(new Error("图片加载失败"));
|
||||
image.src = buildImageProxyUrl(url, {
|
||||
width: MERGED_IMAGE_MAX_WIDTH,
|
||||
quality: 92,
|
||||
format: "png",
|
||||
});
|
||||
image.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadImage(url: string) {
|
||||
const normalizedSource = String(url || "").trim();
|
||||
if (!normalizedSource) {
|
||||
throw new Error("图片地址为空");
|
||||
}
|
||||
|
||||
const candidateUrls = Array.from(
|
||||
new Set(
|
||||
[
|
||||
buildImageProxyUrl(normalizedSource, {
|
||||
width: MERGED_IMAGE_MAX_WIDTH,
|
||||
quality: 92,
|
||||
format: "png",
|
||||
}),
|
||||
buildImageProxyUrl(normalizedSource, {
|
||||
width: Math.min(MERGED_IMAGE_MAX_WIDTH, 960),
|
||||
quality: 86,
|
||||
format: "webp",
|
||||
}),
|
||||
normalizedSource,
|
||||
].filter((item): item is string => Boolean(item))
|
||||
)
|
||||
);
|
||||
|
||||
const errorMessages: string[] = [];
|
||||
|
||||
for (const candidateUrl of candidateUrls) {
|
||||
try {
|
||||
const response = await fetch(candidateUrl, {
|
||||
cache: "force-cache",
|
||||
credentials: "same-origin",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
if (!blob.type.startsWith("image/")) {
|
||||
throw new Error("返回内容不是图片");
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
try {
|
||||
const image = await createImageElement(objectUrl, false);
|
||||
return {
|
||||
image,
|
||||
revokeObjectUrl: () => URL.revokeObjectURL(objectUrl),
|
||||
};
|
||||
} catch (error) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessages.push(
|
||||
error instanceof Error ? error.message : `无法加载图片资源: ${candidateUrl}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const candidateUrl of candidateUrls) {
|
||||
try {
|
||||
const image = await createImageElement(
|
||||
candidateUrl,
|
||||
!candidateUrl.startsWith("data:") && !candidateUrl.startsWith("blob:")
|
||||
);
|
||||
return {
|
||||
image,
|
||||
revokeObjectUrl: null,
|
||||
};
|
||||
} catch (error) {
|
||||
errorMessages.push(
|
||||
error instanceof Error ? error.message : `无法直接加载图片资源: ${candidateUrl}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(errorMessages[0] || "图片加载失败");
|
||||
}
|
||||
|
||||
function buildMergedSnapshot(images: LoadedReferenceImage[]) {
|
||||
const baseWidth = Math.max(
|
||||
1,
|
||||
@ -155,7 +233,9 @@ export function AgentDetailReferenceStitchDialog({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
const [applyProgressText, setApplyProgressText] = useState("");
|
||||
const [loadingProgressText, setLoadingProgressText] = useState("");
|
||||
const [errorText, setErrorText] = useState<string | null>(null);
|
||||
const [warningText, setWarningText] = useState<string | null>(null);
|
||||
|
||||
const validCards = useMemo(
|
||||
() =>
|
||||
@ -210,6 +290,8 @@ export function AgentDetailReferenceStitchDialog({
|
||||
setDraggingLineIndex(null);
|
||||
setIsAddingCutLine(false);
|
||||
setPreviewCutLineY(null);
|
||||
setLoadingProgressText("");
|
||||
setWarningText(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -223,29 +305,54 @@ export function AgentDetailReferenceStitchDialog({
|
||||
let cancelled = false;
|
||||
|
||||
const setup = async () => {
|
||||
let loadedImages: LoadedReferenceImage[] = [];
|
||||
let snapshotCommitted = false;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setErrorText(null);
|
||||
setWarningText(null);
|
||||
|
||||
const loadedImages = await Promise.all(
|
||||
validCards.map(async (card) => {
|
||||
const image = await loadImage(card.imageUrl);
|
||||
return {
|
||||
const failedCards: string[] = [];
|
||||
|
||||
for (const [index, card] of validCards.entries()) {
|
||||
setLoadingProgressText(`正在加载参考图 ${index + 1}/${validCards.length}`);
|
||||
try {
|
||||
const loaded = await loadImage(card.imageUrl);
|
||||
loadedImages.push({
|
||||
card,
|
||||
image,
|
||||
width: image.naturalWidth,
|
||||
height: image.naturalHeight,
|
||||
scaledHeight: image.naturalHeight,
|
||||
} satisfies LoadedReferenceImage;
|
||||
})
|
||||
);
|
||||
image: loaded.image,
|
||||
width: loaded.image.naturalWidth,
|
||||
height: loaded.image.naturalHeight,
|
||||
scaledHeight: loaded.image.naturalHeight,
|
||||
revokeObjectUrl: loaded.revokeObjectUrl,
|
||||
} satisfies LoadedReferenceImage);
|
||||
} catch (error) {
|
||||
failedCards.push(card.label || `第 ${index + 1} 张参考图`);
|
||||
console.error("[AgentDetailReferenceStitchDialog] Failed to load reference image:", {
|
||||
cardId: card.id,
|
||||
label: card.label,
|
||||
imageUrl: card.imageUrl,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
if (loadedImages.length === 0) {
|
||||
throw new Error("当前参考图加载失败,请稍后重试");
|
||||
}
|
||||
|
||||
const next = buildMergedSnapshot(loadedImages);
|
||||
mergedCanvasRef.current = next.canvas;
|
||||
setSnapshot(next.snapshot);
|
||||
setCutLines(next.snapshot.defaultCutLines);
|
||||
snapshotCommitted = true;
|
||||
setWarningText(
|
||||
failedCards.length > 0
|
||||
? `有 ${failedCards.length} 张参考图加载失败,已自动跳过异常图片。`
|
||||
: null
|
||||
);
|
||||
} catch (error) {
|
||||
if (cancelled) return;
|
||||
console.error("[AgentDetailReferenceStitchDialog] Failed to prepare merged image:", error);
|
||||
@ -253,8 +360,12 @@ export function AgentDetailReferenceStitchDialog({
|
||||
setCutLines([]);
|
||||
setErrorText(error instanceof Error ? error.message : "拼合参考图失败");
|
||||
} finally {
|
||||
if (!snapshotCommitted) {
|
||||
loadedImages.forEach((item) => item.revokeObjectUrl?.());
|
||||
}
|
||||
if (!cancelled) {
|
||||
setIsLoading(false);
|
||||
setLoadingProgressText("");
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -266,6 +377,12 @@ export function AgentDetailReferenceStitchDialog({
|
||||
};
|
||||
}, [open, sourceLabel, validCards]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
snapshot?.loadedImages.forEach((item) => item.revokeObjectUrl?.());
|
||||
};
|
||||
}, [snapshot]);
|
||||
|
||||
const resolveInsertCutLineY = useCallback(
|
||||
(rawY: number) => {
|
||||
if (!snapshot) return null;
|
||||
@ -576,7 +693,9 @@ export function AgentDetailReferenceStitchDialog({
|
||||
{isLoading ? (
|
||||
<div className="flex min-h-[420px] flex-col items-center justify-center gap-3 rounded-[24px] border border-dashed border-white/12 bg-black/20 text-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-emerald-300" />
|
||||
<div className="text-sm text-white/75">正在拼合参考图...</div>
|
||||
<div className="text-sm text-white/75">
|
||||
{loadingProgressText || "正在拼合参考图..."}
|
||||
</div>
|
||||
<div className="text-xs text-white/42">会按当前参考图顺序从上到下拼成一张长图</div>
|
||||
</div>
|
||||
) : errorText ? (
|
||||
@ -588,6 +707,11 @@ export function AgentDetailReferenceStitchDialog({
|
||||
</div>
|
||||
) : snapshot ? (
|
||||
<div className="space-y-4">
|
||||
{warningText ? (
|
||||
<div className="rounded-[18px] border border-amber-400/20 bg-amber-500/[0.08] px-4 py-3 text-xs leading-6 text-amber-100/88">
|
||||
{warningText}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
ref={previewContainerRef}
|
||||
className="relative overflow-hidden rounded-[24px] border border-white/10 bg-white"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user