feat: enable remote image url import

This commit is contained in:
Codex
2026-06-12 00:04:07 +08:00
parent 9ff3a6880b
commit 9fb042f950
+107 -4
View File
@@ -998,6 +998,22 @@ function getImageFileFormat(file: File) {
return file.name.split(".").pop()?.toUpperCase() ?? "";
}
function getRemoteImageFormat(mimeType: string, imageUrl: string) {
const mimeFormat = mimeType.split("/")[1]?.replace("jpeg", "jpg").toUpperCase();
if (mimeFormat) return mimeFormat;
return imageUrl.split("?")[0].split(".").pop()?.toUpperCase() ?? "IMAGE";
}
function getRemoteImageName(imageUrl: string, fallback: string) {
try {
const parsed = new URL(imageUrl);
const filename = decodeURIComponent(parsed.pathname.split("/").filter(Boolean).pop() || "");
return filename || fallback;
} catch {
return fallback;
}
}
function readImageDimensions(src: string): Promise<{ width: number; height: number }> {
return new Promise((resolve, reject) => {
const image = new Image();
@@ -1219,7 +1235,9 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const cloneReferenceInputRef = useRef<HTMLInputElement>(null);
const smartCutoutInputRef = useRef<HTMLInputElement>(null);
const imageWorkbenchInputRef = useRef<HTMLInputElement>(null);
const imageWorkbenchUrlInputRef = useRef<HTMLInputElement>(null);
const watermarkInputRef = useRef<HTMLInputElement>(null);
const watermarkUrlInputRef = useRef<HTMLInputElement>(null);
const watermarkProcessTimeoutRef = useRef<number | null>(null);
const smartCutoutTransitionTimeoutRef = useRef<number | null>(null);
const smartCutoutPendingUrlsRef = useRef<string[]>([]);
@@ -1793,6 +1811,49 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
setIsCloneSettingsCollapsed(false);
};
const loadRemoteImageFromInput = async (input: HTMLInputElement | null, fallbackName: string) => {
const rawValue = input?.value.trim() ?? "";
if (!rawValue) {
toast.info("请先粘贴图片 URL");
return null;
}
let imageUrl: string;
try {
const parsed = new URL(rawValue, window.location.href);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new Error("仅支持 http 或 https 图片链接");
}
imageUrl = parsed.toString();
} catch (error) {
toast.error(error instanceof Error ? error.message : "图片 URL 不正确");
return null;
}
try {
const response = await fetch(imageUrl);
if (!response.ok) throw new Error(`图片读取失败(${response.status}`);
const blob = await response.blob();
if (!blob.type.startsWith("image/")) throw new Error("链接内容不是图片");
const src = URL.createObjectURL(blob);
try {
await readImageDimensions(src);
} catch {
URL.revokeObjectURL(src);
throw new Error("图片无法预览,请换一个链接");
}
if (input) input.value = "";
return {
src,
name: getRemoteImageName(imageUrl, fallbackName),
format: getRemoteImageFormat(blob.type, imageUrl),
};
} catch (error) {
toast.error(error instanceof Error ? error.message : "图片导入失败");
return null;
}
};
const closeWatermarkRemovalPage = () => {
if (watermarkProcessTimeoutRef.current !== null) {
window.clearTimeout(watermarkProcessTimeoutRef.current);
@@ -1846,6 +1907,18 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
if (file) addWatermarkImage(file);
};
const handleWatermarkUrlImport = async () => {
const nextImage = await loadRemoteImageFromInput(watermarkUrlInputRef.current, "watermark-source");
if (!nextImage) return;
setWatermarkImage((current) => {
if (current?.src) URL.revokeObjectURL(current.src);
return nextImage;
});
setWatermarkStatus("idle");
setActiveQuickTool("watermark");
toast.success("图片已导入");
};
const handleWatermarkGenerate = () => {
if (!watermarkImage || watermarkStatus === "processing") return;
if (watermarkProcessTimeoutRef.current !== null) window.clearTimeout(watermarkProcessTimeoutRef.current);
@@ -1943,6 +2016,22 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
if (file) addImageWorkbenchImage(file);
};
const handleImageWorkbenchUrlImport = async () => {
const nextImage = await loadRemoteImageFromInput(imageWorkbenchUrlInputRef.current, "image-workbench-source");
if (!nextImage) return;
setImageWorkbenchImage((current) => {
if (current?.src) URL.revokeObjectURL(current.src);
return nextImage;
});
setImageWorkbenchStatus("idle");
setImageWorkbenchMaskStrokes([]);
setImageWorkbenchBrushCursor(null);
clearImageWorkbenchMaskCanvas();
imageWorkbenchActiveStrokeIdRef.current = null;
setActiveQuickTool("image-edit");
toast.success("图片已导入");
};
const handleImageWorkbenchGenerate = () => {
if (!imageWorkbenchImage) {
toast.info("请先上传图片");
@@ -5021,8 +5110,15 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
)}
</div>
<div className="ecom-image-workbench-url-row">
<input placeholder="粘贴图片 URL" aria-label="粘贴图片 URL" />
<button type="button" onClick={() => toast.info("请先使用本地上传")}></button>
<input
ref={imageWorkbenchUrlInputRef}
placeholder="粘贴图片 URL"
aria-label="粘贴图片 URL"
onKeyDown={(event) => {
if (event.key === "Enter") void handleImageWorkbenchUrlImport();
}}
/>
<button type="button" onClick={() => void handleImageWorkbenchUrlImport()}></button>
</div>
</section>
@@ -5225,8 +5321,15 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
)}
</div>
<div className="ecom-watermark-url-row">
<input placeholder="粘贴图片 URL" aria-label="粘贴图片 URL" />
<button type="button" onClick={() => toast.info("请先使用本地上传")}></button>
<input
ref={watermarkUrlInputRef}
placeholder="粘贴图片 URL"
aria-label="粘贴图片 URL"
onKeyDown={(event) => {
if (event.key === "Enter") void handleWatermarkUrlImport();
}}
/>
<button type="button" onClick={() => void handleWatermarkUrlImport()}></button>
</div>
</section>