feat: enable remote image url import
This commit is contained in:
@@ -998,6 +998,22 @@ function getImageFileFormat(file: File) {
|
|||||||
return file.name.split(".").pop()?.toUpperCase() ?? "";
|
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 }> {
|
function readImageDimensions(src: string): Promise<{ width: number; height: number }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
@@ -1219,7 +1235,9 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
const cloneReferenceInputRef = useRef<HTMLInputElement>(null);
|
const cloneReferenceInputRef = useRef<HTMLInputElement>(null);
|
||||||
const smartCutoutInputRef = useRef<HTMLInputElement>(null);
|
const smartCutoutInputRef = useRef<HTMLInputElement>(null);
|
||||||
const imageWorkbenchInputRef = useRef<HTMLInputElement>(null);
|
const imageWorkbenchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const imageWorkbenchUrlInputRef = useRef<HTMLInputElement>(null);
|
||||||
const watermarkInputRef = useRef<HTMLInputElement>(null);
|
const watermarkInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const watermarkUrlInputRef = useRef<HTMLInputElement>(null);
|
||||||
const watermarkProcessTimeoutRef = useRef<number | null>(null);
|
const watermarkProcessTimeoutRef = useRef<number | null>(null);
|
||||||
const smartCutoutTransitionTimeoutRef = useRef<number | null>(null);
|
const smartCutoutTransitionTimeoutRef = useRef<number | null>(null);
|
||||||
const smartCutoutPendingUrlsRef = useRef<string[]>([]);
|
const smartCutoutPendingUrlsRef = useRef<string[]>([]);
|
||||||
@@ -1793,6 +1811,49 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
setIsCloneSettingsCollapsed(false);
|
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 = () => {
|
const closeWatermarkRemovalPage = () => {
|
||||||
if (watermarkProcessTimeoutRef.current !== null) {
|
if (watermarkProcessTimeoutRef.current !== null) {
|
||||||
window.clearTimeout(watermarkProcessTimeoutRef.current);
|
window.clearTimeout(watermarkProcessTimeoutRef.current);
|
||||||
@@ -1846,6 +1907,18 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
if (file) addWatermarkImage(file);
|
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 = () => {
|
const handleWatermarkGenerate = () => {
|
||||||
if (!watermarkImage || watermarkStatus === "processing") return;
|
if (!watermarkImage || watermarkStatus === "processing") return;
|
||||||
if (watermarkProcessTimeoutRef.current !== null) window.clearTimeout(watermarkProcessTimeoutRef.current);
|
if (watermarkProcessTimeoutRef.current !== null) window.clearTimeout(watermarkProcessTimeoutRef.current);
|
||||||
@@ -1943,6 +2016,22 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
if (file) addImageWorkbenchImage(file);
|
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 = () => {
|
const handleImageWorkbenchGenerate = () => {
|
||||||
if (!imageWorkbenchImage) {
|
if (!imageWorkbenchImage) {
|
||||||
toast.info("请先上传图片");
|
toast.info("请先上传图片");
|
||||||
@@ -5021,8 +5110,15 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="ecom-image-workbench-url-row">
|
<div className="ecom-image-workbench-url-row">
|
||||||
<input placeholder="粘贴图片 URL" aria-label="粘贴图片 URL" />
|
<input
|
||||||
<button type="button" onClick={() => toast.info("请先使用本地上传")}>添加</button>
|
ref={imageWorkbenchUrlInputRef}
|
||||||
|
placeholder="粘贴图片 URL"
|
||||||
|
aria-label="粘贴图片 URL"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") void handleImageWorkbenchUrlImport();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button type="button" onClick={() => void handleImageWorkbenchUrlImport()}>添加</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -5225,8 +5321,15 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="ecom-watermark-url-row">
|
<div className="ecom-watermark-url-row">
|
||||||
<input placeholder="粘贴图片 URL" aria-label="粘贴图片 URL" />
|
<input
|
||||||
<button type="button" onClick={() => toast.info("请先使用本地上传")}>导入</button>
|
ref={watermarkUrlInputRef}
|
||||||
|
placeholder="粘贴图片 URL"
|
||||||
|
aria-label="粘贴图片 URL"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") void handleWatermarkUrlImport();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button type="button" onClick={() => void handleWatermarkUrlImport()}>导入</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user