From 9fb042f950bace5d6496757ae244222ed04cf79a Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 12 Jun 2026 00:04:07 +0800 Subject: [PATCH] feat: enable remote image url import --- src/features/ecommerce/EcommercePage.tsx | 111 ++++++++++++++++++++++- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index af4a790..d1bf1d3 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -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(null); const smartCutoutInputRef = useRef(null); const imageWorkbenchInputRef = useRef(null); + const imageWorkbenchUrlInputRef = useRef(null); const watermarkInputRef = useRef(null); + const watermarkUrlInputRef = useRef(null); const watermarkProcessTimeoutRef = useRef(null); const smartCutoutTransitionTimeoutRef = useRef(null); const smartCutoutPendingUrlsRef = useRef([]); @@ -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 = {}) { )}
- - + { + if (event.key === "Enter") void handleImageWorkbenchUrlImport(); + }} + /> +
@@ -5225,8 +5321,15 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { )}
- - + { + if (event.key === "Enter") void handleWatermarkUrlImport(); + }} + /> +