feat: 电商快捷工具接入真实API并增强预览交互
- 图片修改接入局部重绘API,改为左右对比布局 - 去水印接入真实API,带进度条 - A+详情页预览区增加生成中/失败状态与进度条 - 新增图片翻译页面(含语言选择器) - 快捷功能栏改为一行五列均分布局,移除白框 - 预览弹窗与A+详情页结果增加保存本地按钮
This commit is contained in:
@@ -1333,9 +1333,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const smartCutoutInputRef = useRef<HTMLInputElement>(null);
|
||||
const imageWorkbenchInputRef = useRef<HTMLInputElement>(null);
|
||||
const imageWorkbenchUrlInputRef = useRef<HTMLInputElement>(null);
|
||||
const imageWorkbenchProgressRef = useRef<number | null>(null);
|
||||
const watermarkInputRef = useRef<HTMLInputElement>(null);
|
||||
const watermarkUrlInputRef = useRef<HTMLInputElement>(null);
|
||||
const watermarkProcessTimeoutRef = useRef<number | null>(null);
|
||||
const translateInputRef = useRef<HTMLInputElement>(null);
|
||||
const translateUrlInputRef = useRef<HTMLInputElement>(null);
|
||||
const translateProcessTimeoutRef = useRef<number | null>(null);
|
||||
const smartCutoutTransitionTimeoutRef = useRef<number | null>(null);
|
||||
const smartCutoutPendingUrlsRef = useRef<string[]>([]);
|
||||
const smartCutoutPaletteRef = useRef<HTMLDivElement>(null);
|
||||
@@ -1345,6 +1349,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const commandComposerWrapRef = useRef<HTMLElement | null>(null);
|
||||
const garmentInputRef = useRef<HTMLInputElement>(null);
|
||||
const detailInputRef = useRef<HTMLInputElement>(null);
|
||||
const detailProgressRef = useRef<number | null>(null);
|
||||
const countHoldTimeoutRef = useRef<number | null>(null);
|
||||
const countHoldIntervalRef = useRef<number | null>(null);
|
||||
const isAuthenticated = Boolean((_props as Record<string, unknown>).isAuthenticated);
|
||||
@@ -1375,7 +1380,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const [selectedProductSetPreview, setSelectedProductSetPreview] = useState<{ src: string; label: string } | null>(null);
|
||||
const [showHostingModal, setShowHostingModal] = useState(false);
|
||||
const [productImages, setProductImages] = useState<CloneImageItem[]>([]);
|
||||
const [activeQuickTool, setActiveQuickTool] = useState<"cutout" | "detail" | "watermark" | "image-edit" | null>(null);
|
||||
const [activeQuickTool, setActiveQuickTool] = useState<"cutout" | "detail" | "watermark" | "image-edit" | "translate" | null>(null);
|
||||
const [smartCutoutImage, setSmartCutoutImage] = useState<SmartCutoutImageItem | null>(null);
|
||||
const [smartCutoutBatchImages, setSmartCutoutBatchImages] = useState<SmartCutoutImageItem[]>([]);
|
||||
const [smartCutoutBackgroundColor, setSmartCutoutBackgroundColor] = useState("#ffffff");
|
||||
@@ -1391,16 +1396,24 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
subtitle: "请稍候",
|
||||
});
|
||||
const [watermarkImage, setWatermarkImage] = useState<{ src: string; name: string; format: string } | null>(null);
|
||||
const [watermarkStatus, setWatermarkStatus] = useState<"idle" | "processing" | "done">("idle");
|
||||
const [watermarkStatus, setWatermarkStatus] = useState<"idle" | "processing" | "done" | "failed">("idle");
|
||||
const [isWatermarkDragging, setIsWatermarkDragging] = useState(false);
|
||||
const [watermarkResultUrl, setWatermarkResultUrl] = useState<string | null>(null);
|
||||
const [watermarkProgress, setWatermarkProgress] = useState(0);
|
||||
const [translateImage, setTranslateImage] = useState<{ src: string; name: string; format: string } | null>(null);
|
||||
const [translateStatus, setTranslateStatus] = useState<"idle" | "processing" | "done">("idle");
|
||||
const [isTranslateDragging, setIsTranslateDragging] = useState(false);
|
||||
const [translateLanguage, setTranslateLanguage] = useState("zh");
|
||||
const [imageWorkbenchImage, setImageWorkbenchImage] = useState<{ src: string; name: string; format: string } | null>(null);
|
||||
const [imageWorkbenchPrompt, setImageWorkbenchPrompt] = useState("");
|
||||
const [imageWorkbenchBrushSize, setImageWorkbenchBrushSize] = useState(50);
|
||||
const [imageWorkbenchRatio, setImageWorkbenchRatio] = useState("1:1");
|
||||
const [imageWorkbenchStatus, setImageWorkbenchStatus] = useState<"idle" | "processing" | "done">("idle");
|
||||
const [imageWorkbenchStatus, setImageWorkbenchStatus] = useState<"idle" | "processing" | "done" | "failed">("idle");
|
||||
const [isImageWorkbenchDragging, setIsImageWorkbenchDragging] = useState(false);
|
||||
const [imageWorkbenchMaskStrokes, setImageWorkbenchMaskStrokes] = useState<Array<{ id: string; size: number; points: Array<{ x: number; y: number }> }>>([]);
|
||||
const [imageWorkbenchBrushCursor, setImageWorkbenchBrushCursor] = useState<{ x: number; y: number } | null>(null);
|
||||
const [imageWorkbenchResultUrl, setImageWorkbenchResultUrl] = useState<string | null>(null);
|
||||
const [imageWorkbenchProgress, setImageWorkbenchProgress] = useState(0);
|
||||
const [isProductUploadDragging, setIsProductUploadDragging] = useState(false);
|
||||
const [cloneOutput, setCloneOutput] = useState<CloneOutputKey>(defaultCloneOutput);
|
||||
const [videoHistoryVisible, setVideoHistoryVisible] = useState(false);
|
||||
@@ -1699,6 +1712,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const [selectedDetailModules, setSelectedDetailModules] = useState<string[]>(defaultDetailModuleIds);
|
||||
const [detailStatus, setDetailStatus] = useState<DetailStatus>("idle");
|
||||
const [detailResultUrl, setDetailResultUrl] = useState<string | null>(null);
|
||||
const [detailProgress, setDetailProgress] = useState(0);
|
||||
const productSetRatioOptions = useMemo(
|
||||
() => getPlatformRatioOptions(productSetPlatform, productSetOutput),
|
||||
[productSetOutput, productSetPlatform],
|
||||
@@ -1941,12 +1955,11 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
};
|
||||
|
||||
const closeWatermarkRemovalPage = () => {
|
||||
if (watermarkProcessTimeoutRef.current !== null) {
|
||||
window.clearTimeout(watermarkProcessTimeoutRef.current);
|
||||
watermarkProcessTimeoutRef.current = null;
|
||||
}
|
||||
stopWatermarkProgress();
|
||||
setActiveQuickTool(null);
|
||||
setWatermarkStatus("idle");
|
||||
setWatermarkResultUrl(null);
|
||||
setWatermarkProgress(0);
|
||||
setWatermarkImage((current) => {
|
||||
if (current?.src) URL.revokeObjectURL(current.src);
|
||||
return null;
|
||||
@@ -1964,15 +1977,16 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
return nextImage;
|
||||
});
|
||||
setWatermarkStatus("idle");
|
||||
setWatermarkResultUrl(null);
|
||||
setWatermarkProgress(0);
|
||||
setActiveQuickTool("watermark");
|
||||
};
|
||||
|
||||
const removeWatermarkImage = () => {
|
||||
if (watermarkProcessTimeoutRef.current !== null) {
|
||||
window.clearTimeout(watermarkProcessTimeoutRef.current);
|
||||
watermarkProcessTimeoutRef.current = null;
|
||||
}
|
||||
stopWatermarkProgress();
|
||||
setWatermarkStatus("idle");
|
||||
setWatermarkResultUrl(null);
|
||||
setWatermarkProgress(0);
|
||||
setWatermarkImage((current) => {
|
||||
if (current?.src) URL.revokeObjectURL(current.src);
|
||||
return null;
|
||||
@@ -2005,31 +2019,187 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
toast.success("图片已导入");
|
||||
};
|
||||
|
||||
const handleWatermarkGenerate = () => {
|
||||
if (!watermarkImage || watermarkStatus === "processing") return;
|
||||
if (watermarkProcessTimeoutRef.current !== null) window.clearTimeout(watermarkProcessTimeoutRef.current);
|
||||
setWatermarkStatus("processing");
|
||||
watermarkProcessTimeoutRef.current = window.setTimeout(() => {
|
||||
const stopWatermarkProgress = () => {
|
||||
if (watermarkProcessTimeoutRef.current !== null) {
|
||||
window.clearInterval(watermarkProcessTimeoutRef.current);
|
||||
watermarkProcessTimeoutRef.current = null;
|
||||
setWatermarkStatus("done");
|
||||
toast.success("去水印处理完成");
|
||||
}, 900);
|
||||
}
|
||||
};
|
||||
|
||||
const startWatermarkProgress = () => {
|
||||
stopWatermarkProgress();
|
||||
setWatermarkProgress(0);
|
||||
watermarkProcessTimeoutRef.current = window.setInterval(() => {
|
||||
setWatermarkProgress((prev) => {
|
||||
if (prev >= 90) {
|
||||
stopWatermarkProgress();
|
||||
return 90;
|
||||
}
|
||||
return prev + (90 - prev) * 0.06;
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleWatermarkGenerate = async () => {
|
||||
if (!watermarkImage || watermarkStatus === "processing") return;
|
||||
setWatermarkStatus("processing");
|
||||
setWatermarkResultUrl(null);
|
||||
startWatermarkProgress();
|
||||
|
||||
try {
|
||||
const sourceBlob = await fetch(watermarkImage.src).then((res) => res.blob());
|
||||
const sourceMime = normalizeEcommerceImageMime(sourceBlob.type || "image/png");
|
||||
const { url: imageUrl } = await aiGenerationClient.uploadAssetBinary(sourceBlob, {
|
||||
name: `watermark-source-${Date.now()}.png`,
|
||||
mimeType: sourceMime,
|
||||
scope: ecommerceOssScopes.productSource,
|
||||
});
|
||||
|
||||
const { taskId } = await aiGenerationClient.createImageEditTask({
|
||||
imageUrl,
|
||||
function: "watermark-remove",
|
||||
});
|
||||
|
||||
const resultUrl = await waitForTask(taskId, {
|
||||
abortRef: { current: false },
|
||||
onProgress: () => {},
|
||||
});
|
||||
|
||||
if (resultUrl) {
|
||||
const persistedUrl = await persistGeneratedImageUrl(resultUrl, ecommerceOssScopes.cloneResult("watermark"), "ecommerce-watermark");
|
||||
setWatermarkResultUrl(persistedUrl);
|
||||
setWatermarkStatus("done");
|
||||
stopWatermarkProgress();
|
||||
setWatermarkProgress(100);
|
||||
toast.success("去水印处理完成");
|
||||
} else {
|
||||
setWatermarkStatus("failed");
|
||||
stopWatermarkProgress();
|
||||
setWatermarkProgress(0);
|
||||
toast.error("去水印未返回结果");
|
||||
}
|
||||
} catch (err) {
|
||||
setWatermarkStatus("failed");
|
||||
stopWatermarkProgress();
|
||||
setWatermarkProgress(0);
|
||||
if (err instanceof ServerRequestError && err.status === 402) {
|
||||
toast.error("余额不足,请充值后继续");
|
||||
} else {
|
||||
toast.error(err instanceof Error ? err.message : "去水印失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleWatermarkDownload = () => {
|
||||
if (!watermarkImage || watermarkStatus !== "done") {
|
||||
if (!watermarkResultUrl || watermarkStatus !== "done") {
|
||||
toast.info("请先完成去水印");
|
||||
return;
|
||||
}
|
||||
const link = document.createElement("a");
|
||||
const safeName = (watermarkImage.name || "watermark-result").replace(/\.[^.]+$/, "").replace(/[\\/:*?"<>|]+/g, "-");
|
||||
link.href = watermarkImage.src;
|
||||
const safeName = (watermarkImage?.name || "watermark-result").replace(/\.[^.]+$/, "").replace(/[\\/:*?"<>|]+/g, "-");
|
||||
link.href = watermarkResultUrl;
|
||||
link.download = `${safeName || "watermark-result"}-去水印.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
};
|
||||
|
||||
const openImageTranslatePage = () => {
|
||||
clearSmartCutoutTransition();
|
||||
setActiveQuickTool("translate");
|
||||
setComposerMenu(null);
|
||||
setIsCloneSettingsCollapsed(false);
|
||||
};
|
||||
|
||||
const closeImageTranslatePage = () => {
|
||||
if (translateProcessTimeoutRef.current !== null) {
|
||||
window.clearTimeout(translateProcessTimeoutRef.current);
|
||||
translateProcessTimeoutRef.current = null;
|
||||
}
|
||||
setActiveQuickTool(null);
|
||||
setTranslateStatus("idle");
|
||||
setTranslateImage((current) => {
|
||||
if (current?.src) URL.revokeObjectURL(current.src);
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
const addTranslateImage = (file: File) => {
|
||||
const nextImage = {
|
||||
src: URL.createObjectURL(file),
|
||||
name: file.name,
|
||||
format: getImageFileFormat(file) || "PNG / JPG / WebP",
|
||||
};
|
||||
setTranslateImage((current) => {
|
||||
if (current?.src) URL.revokeObjectURL(current.src);
|
||||
return nextImage;
|
||||
});
|
||||
setTranslateStatus("idle");
|
||||
setActiveQuickTool("translate");
|
||||
};
|
||||
|
||||
const removeTranslateImage = () => {
|
||||
if (translateProcessTimeoutRef.current !== null) {
|
||||
window.clearTimeout(translateProcessTimeoutRef.current);
|
||||
translateProcessTimeoutRef.current = null;
|
||||
}
|
||||
setTranslateStatus("idle");
|
||||
setTranslateImage((current) => {
|
||||
if (current?.src) URL.revokeObjectURL(current.src);
|
||||
return null;
|
||||
});
|
||||
};
|
||||
|
||||
const handleTranslateUpload = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
addTranslateImage(file);
|
||||
event.target.value = "";
|
||||
};
|
||||
|
||||
const handleTranslateDrop = (event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setIsTranslateDragging(false);
|
||||
const file = Array.from(event.dataTransfer.files).find((item) => item.type.startsWith("image/"));
|
||||
if (file) addTranslateImage(file);
|
||||
};
|
||||
|
||||
const handleTranslateUrlImport = async () => {
|
||||
const nextImage = await loadRemoteImageFromInput(translateUrlInputRef.current, "translate-source");
|
||||
if (!nextImage) return;
|
||||
setTranslateImage((current) => {
|
||||
if (current?.src) URL.revokeObjectURL(current.src);
|
||||
return nextImage;
|
||||
});
|
||||
setTranslateStatus("idle");
|
||||
toast.success("图片已导入");
|
||||
};
|
||||
|
||||
const handleTranslateGenerate = () => {
|
||||
if (!translateImage || translateStatus === "processing") return;
|
||||
if (translateProcessTimeoutRef.current !== null) window.clearTimeout(translateProcessTimeoutRef.current);
|
||||
setTranslateStatus("processing");
|
||||
translateProcessTimeoutRef.current = window.setTimeout(() => {
|
||||
translateProcessTimeoutRef.current = null;
|
||||
setTranslateStatus("done");
|
||||
toast.success("图片翻译完成");
|
||||
}, 900);
|
||||
};
|
||||
|
||||
const handleTranslateDownload = () => {
|
||||
if (!translateImage || translateStatus !== "done") {
|
||||
toast.info("请先完成图片翻译");
|
||||
return;
|
||||
}
|
||||
const link = document.createElement("a");
|
||||
const safeName = (translateImage.name || "translate-result").replace(/\.[^.]+$/, "").replace(/[\\/:*?"<>|]+/g, "-");
|
||||
link.href = translateImage.src;
|
||||
link.download = `${safeName || "translate-result"}-翻译.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
};
|
||||
|
||||
const openImageWorkbenchPage = () => {
|
||||
clearSmartCutoutTransition();
|
||||
setActiveQuickTool("image-edit");
|
||||
@@ -2041,6 +2211,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const closeImageWorkbenchPage = () => {
|
||||
setActiveQuickTool(null);
|
||||
setImageWorkbenchStatus("idle");
|
||||
setImageWorkbenchResultUrl(null);
|
||||
setImageWorkbenchPrompt("");
|
||||
setImageWorkbenchMaskStrokes([]);
|
||||
setImageWorkbenchBrushCursor(null);
|
||||
@@ -2068,6 +2239,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
return nextImage;
|
||||
});
|
||||
setImageWorkbenchStatus("idle");
|
||||
setImageWorkbenchResultUrl(null);
|
||||
setImageWorkbenchMaskStrokes([]);
|
||||
setImageWorkbenchBrushCursor(null);
|
||||
clearImageWorkbenchMaskCanvas();
|
||||
@@ -2077,6 +2249,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
const removeImageWorkbenchImage = () => {
|
||||
setImageWorkbenchStatus("idle");
|
||||
setImageWorkbenchResultUrl(null);
|
||||
setImageWorkbenchMaskStrokes([]);
|
||||
setImageWorkbenchBrushCursor(null);
|
||||
clearImageWorkbenchMaskCanvas();
|
||||
@@ -2118,16 +2291,123 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
toast.success("图片已导入");
|
||||
};
|
||||
|
||||
const handleImageWorkbenchGenerate = () => {
|
||||
const stopWorkbenchProgress = () => {
|
||||
if (imageWorkbenchProgressRef.current !== null) {
|
||||
window.clearInterval(imageWorkbenchProgressRef.current);
|
||||
imageWorkbenchProgressRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startWorkbenchProgress = () => {
|
||||
stopWorkbenchProgress();
|
||||
setImageWorkbenchProgress(0);
|
||||
imageWorkbenchProgressRef.current = window.setInterval(() => {
|
||||
setImageWorkbenchProgress((prev) => {
|
||||
if (prev >= 90) {
|
||||
stopWorkbenchProgress();
|
||||
return 90;
|
||||
}
|
||||
return prev + (90 - prev) * 0.06;
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const exportWorkbenchMask = (): string | null => {
|
||||
const canvas = imageWorkbenchMaskCanvasRef.current;
|
||||
if (!canvas) return null;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return null;
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const maskCanvas = document.createElement("canvas");
|
||||
maskCanvas.width = w;
|
||||
maskCanvas.height = h;
|
||||
const maskCtx = maskCanvas.getContext("2d")!;
|
||||
maskCtx.fillStyle = "#000000";
|
||||
maskCtx.fillRect(0, 0, w, h);
|
||||
const imgData = ctx.getImageData(0, 0, w, h);
|
||||
const maskData = maskCtx.getImageData(0, 0, w, h);
|
||||
for (let i = 3; i < imgData.data.length; i += 4) {
|
||||
if (imgData.data[i] > 0) {
|
||||
const pi = i - 3;
|
||||
maskData.data[pi] = 255;
|
||||
maskData.data[pi + 1] = 255;
|
||||
maskData.data[pi + 2] = 255;
|
||||
maskData.data[pi + 3] = 255;
|
||||
}
|
||||
}
|
||||
maskCtx.putImageData(maskData, 0, 0);
|
||||
return maskCanvas.toDataURL("image/png");
|
||||
};
|
||||
|
||||
const handleImageWorkbenchGenerate = async () => {
|
||||
if (!imageWorkbenchImage) {
|
||||
toast.info("请先上传图片");
|
||||
return;
|
||||
}
|
||||
setImageWorkbenchStatus("processing");
|
||||
window.setTimeout(() => {
|
||||
setImageWorkbenchStatus("done");
|
||||
toast.success("局部重绘已完成");
|
||||
}, 900);
|
||||
setImageWorkbenchResultUrl(null);
|
||||
startWorkbenchProgress();
|
||||
|
||||
try {
|
||||
const sourceBlob = await fetch(imageWorkbenchImage.src).then((res) => res.blob());
|
||||
const sourceMime = normalizeEcommerceImageMime(sourceBlob.type || "image/png");
|
||||
const { url: imageUrl } = await aiGenerationClient.uploadAssetBinary(sourceBlob, {
|
||||
name: `inpaint-source-${Date.now()}.png`,
|
||||
mimeType: sourceMime,
|
||||
scope: ecommerceOssScopes.productSource,
|
||||
});
|
||||
|
||||
let maskUrl: string | undefined;
|
||||
if (imageWorkbenchMaskStrokes.length > 0) {
|
||||
const maskDataUrl = exportWorkbenchMask();
|
||||
if (maskDataUrl) {
|
||||
const { url } = await aiGenerationClient.uploadAsset({
|
||||
dataUrl: maskDataUrl,
|
||||
name: `inpaint-mask-${Date.now()}.png`,
|
||||
mimeType: "image/png",
|
||||
scope: ecommerceOssScopes.productSource,
|
||||
});
|
||||
maskUrl = url;
|
||||
}
|
||||
}
|
||||
|
||||
const { taskId } = await aiGenerationClient.createImageEditTask({
|
||||
imageUrl,
|
||||
function: "inpaint",
|
||||
prompt: imageWorkbenchPrompt || undefined,
|
||||
maskUrl,
|
||||
ratio: imageWorkbenchRatio,
|
||||
});
|
||||
|
||||
const resultUrl = await waitForTask(taskId, {
|
||||
abortRef: { current: false },
|
||||
onProgress: () => {},
|
||||
});
|
||||
|
||||
if (resultUrl) {
|
||||
const persistedUrl = await persistGeneratedImageUrl(resultUrl, ecommerceOssScopes.cloneResult("inpaint"), "ecommerce-inpaint");
|
||||
setImageWorkbenchResultUrl(persistedUrl);
|
||||
setImageWorkbenchStatus("done");
|
||||
stopWorkbenchProgress();
|
||||
setImageWorkbenchProgress(100);
|
||||
toast.success("局部重绘已完成");
|
||||
} else {
|
||||
setImageWorkbenchStatus("failed");
|
||||
stopWorkbenchProgress();
|
||||
setImageWorkbenchProgress(0);
|
||||
toast.error("重绘未返回结果");
|
||||
}
|
||||
} catch (err) {
|
||||
setImageWorkbenchStatus("failed");
|
||||
stopWorkbenchProgress();
|
||||
setImageWorkbenchProgress(0);
|
||||
if (err instanceof ServerRequestError && err.status === 402) {
|
||||
toast.error("余额不足,请充值后继续");
|
||||
} else {
|
||||
toast.error(err instanceof Error ? err.message : "重绘失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const syncImageWorkbenchMaskCanvas = () => {
|
||||
@@ -3566,15 +3846,46 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
);
|
||||
};
|
||||
|
||||
const stopDetailProgress = () => {
|
||||
if (detailProgressRef.current !== null) {
|
||||
window.clearInterval(detailProgressRef.current);
|
||||
detailProgressRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startDetailProgress = () => {
|
||||
stopDetailProgress();
|
||||
setDetailProgress(0);
|
||||
detailProgressRef.current = window.setInterval(() => {
|
||||
setDetailProgress((prev) => {
|
||||
if (prev >= 90) {
|
||||
stopDetailProgress();
|
||||
return 90;
|
||||
}
|
||||
return prev + (90 - prev) * 0.06;
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleDetailGenerate = () => {
|
||||
if (!canGenerateDetail) return;
|
||||
imageAbortRef.current = { current: false };
|
||||
lastFailedActionRef.current = null;
|
||||
startDetailProgress();
|
||||
void generateEcommerceImage(
|
||||
"detail", detailProductImages, detailRequirement,
|
||||
detailPlatform, detailRatio, detailLanguage, detailMarket,
|
||||
{ detailModules: selectedDetailModules },
|
||||
(s: string) => setDetailStatus(s as DetailStatus),
|
||||
(s: string) => {
|
||||
setDetailStatus(s as DetailStatus);
|
||||
if (s === "done") {
|
||||
stopDetailProgress();
|
||||
setDetailProgress(100);
|
||||
} else if (s === "failed" || s === "idle") {
|
||||
stopDetailProgress();
|
||||
setDetailProgress(0);
|
||||
}
|
||||
},
|
||||
(res) => setDetailResultUrl(res[0]?.src ?? null),
|
||||
);
|
||||
};
|
||||
@@ -3640,6 +3951,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const isQuickDetailTool = isCloneTool && activeQuickTool === "detail";
|
||||
const isWatermarkTool = isCloneTool && activeQuickTool === "watermark";
|
||||
const isImageEditTool = isCloneTool && activeQuickTool === "image-edit";
|
||||
const isTranslateTool = isCloneTool && activeQuickTool === "translate";
|
||||
const pageLabel = isSetTool ? "商品套图" : isDetail ? "A+/详情页" : isTryOn ? "AI服饰穿戴" : activeToolMeta?.label || "商品工具";
|
||||
const setPrimaryLabel =
|
||||
setImages.length === 0
|
||||
@@ -4861,6 +5173,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
{ label: "图片修改", tone: "edit", icon: <EditOutlined />, onClick: openImageWorkbenchPage },
|
||||
{ label: "智能抠图", tone: "cutout", icon: <ScissorOutlined />, onClick: openSmartCutoutUpload },
|
||||
{ label: "去除水印", tone: "watermark", icon: <ClearOutlined />, onClick: openWatermarkRemovalPage },
|
||||
{ label: "图片翻译", tone: "translate", icon: <GlobalOutlined />, onClick: openImageTranslatePage },
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.label}
|
||||
@@ -5302,6 +5615,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
setImageWorkbenchBrushCursor(null);
|
||||
clearImageWorkbenchMaskCanvas();
|
||||
setImageWorkbenchStatus("idle");
|
||||
setImageWorkbenchResultUrl(null);
|
||||
}}
|
||||
disabled={!imageWorkbenchMaskStrokes.length}
|
||||
>
|
||||
@@ -5346,29 +5660,34 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</aside>
|
||||
|
||||
<section className="ecom-image-workbench-stage">
|
||||
<div
|
||||
className={`ecom-image-workbench-canvas${isImageWorkbenchDragging ? " is-dragging" : ""}${imageWorkbenchImage ? " has-image" : ""}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (!imageWorkbenchImage) imageWorkbenchInputRef.current?.click();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (!imageWorkbenchImage && (event.key === "Enter" || event.key === " ")) {
|
||||
{!imageWorkbenchImage ? (
|
||||
<div
|
||||
className={`ecom-watermark-dropzone${isImageWorkbenchDragging ? " is-dragging" : ""}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => imageWorkbenchInputRef.current?.click()}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
imageWorkbenchInputRef.current?.click();
|
||||
}
|
||||
}}
|
||||
onDragEnter={(event) => {
|
||||
event.preventDefault();
|
||||
imageWorkbenchInputRef.current?.click();
|
||||
}
|
||||
}}
|
||||
onDragEnter={(event) => {
|
||||
event.preventDefault();
|
||||
setIsImageWorkbenchDragging(true);
|
||||
}}
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
onDragLeave={() => setIsImageWorkbenchDragging(false)}
|
||||
onDrop={handleImageWorkbenchDrop}
|
||||
>
|
||||
{imageWorkbenchImage ? (
|
||||
<div className="ecom-image-workbench-preview">
|
||||
setIsImageWorkbenchDragging(true);
|
||||
}}
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
onDragLeave={() => setIsImageWorkbenchDragging(false)}
|
||||
onDrop={handleImageWorkbenchDrop}
|
||||
>
|
||||
<CloudUploadOutlined />
|
||||
<strong>点击或拖拽上传图片</strong>
|
||||
<span>支持 PNG / JPG / WebP,上传后使用画笔标记需要重绘的区域</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ecom-watermark-grid">
|
||||
<article className="ecom-watermark-preview-card">
|
||||
<span>原图 / 涂抹区域</span>
|
||||
<div
|
||||
className="ecom-image-workbench-image-frame"
|
||||
onPointerDown={handleImageWorkbenchMaskPointerDown}
|
||||
@@ -5392,16 +5711,58 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<em>{imageWorkbenchStatus === "done" ? "重绘预览" : imageWorkbenchStatus === "processing" ? "正在生成重绘结果" : "按住鼠标涂抹需要修改的区域"}</em>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ecom-image-workbench-empty">
|
||||
<FileImageOutlined />
|
||||
<strong>点击或拖拽上传图片</strong>
|
||||
<span>支持 PNG / JPG / WebP,上传后使用画笔标记需要重绘的区域</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="ecom-watermark-preview-card">
|
||||
<span>重绘结果</span>
|
||||
{imageWorkbenchStatus === "processing" ? (
|
||||
<div className="ecom-watermark-processing" role="status" aria-live="polite">
|
||||
<LoadingOutlined />
|
||||
<strong>正在重绘</strong>
|
||||
<em>AI 正在根据遮罩和提示词生成局部重绘结果</em>
|
||||
<div className="ecom-quick-set-progress">
|
||||
<div className="ecom-quick-set-progress-bar" style={{ width: `${Math.round(imageWorkbenchProgress)}%` }} />
|
||||
</div>
|
||||
<em className="ecom-quick-set-progress-text">{Math.round(imageWorkbenchProgress)}%</em>
|
||||
</div>
|
||||
) : imageWorkbenchStatus === "done" && imageWorkbenchResultUrl ? (
|
||||
<>
|
||||
<img src={imageWorkbenchResultUrl} alt="重绘结果" />
|
||||
</>
|
||||
) : imageWorkbenchStatus === "failed" ? (
|
||||
<div className="ecom-watermark-empty">
|
||||
<FrownOutlined />
|
||||
<strong>重绘失败</strong>
|
||||
<em>请检查网络或重试,如余额不足请先充值</em>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ecom-watermark-empty">
|
||||
<FileImageOutlined />
|
||||
<strong>等待处理</strong>
|
||||
<em>涂抹需要修改的区域后点击开始重绘</em>
|
||||
</div>
|
||||
)}
|
||||
<div className="ecom-watermark-actions">
|
||||
<button type="button" onClick={() => toast.success("已加入资产库")} disabled={imageWorkbenchStatus !== "done"}>
|
||||
<FolderOpenOutlined />
|
||||
加入资产库
|
||||
</button>
|
||||
<button type="button" onClick={() => {
|
||||
if (!imageWorkbenchResultUrl) return;
|
||||
const link = document.createElement("a");
|
||||
link.href = imageWorkbenchResultUrl;
|
||||
link.download = `inpaint-result-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}} disabled={imageWorkbenchStatus !== "done"}>
|
||||
<CloudUploadOutlined />
|
||||
下载图片
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
@@ -5545,14 +5906,24 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
<LoadingOutlined />
|
||||
<strong>正在去水印</strong>
|
||||
<em>AI 正在清理图片中的水印和文字</em>
|
||||
<div className="ecom-quick-set-progress">
|
||||
<div className="ecom-quick-set-progress-bar" style={{ width: `${Math.round(watermarkProgress)}%` }} />
|
||||
</div>
|
||||
<em className="ecom-quick-set-progress-text">{Math.round(watermarkProgress)}%</em>
|
||||
</div>
|
||||
) : watermarkStatus === "done" ? (
|
||||
) : watermarkStatus === "done" && watermarkResultUrl ? (
|
||||
<>
|
||||
<img src={watermarkImage.src} alt="去水印结果" />
|
||||
<img src={watermarkResultUrl} alt="去水印结果" />
|
||||
<button type="button" className="ecom-watermark-zoom" aria-label="查看大图">
|
||||
<QuestionCircleOutlined />
|
||||
</button>
|
||||
</>
|
||||
) : watermarkStatus === "failed" ? (
|
||||
<div className="ecom-watermark-empty">
|
||||
<FrownOutlined />
|
||||
<strong>去水印失败</strong>
|
||||
<em>请检查网络或重试,如余额不足请先充值</em>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ecom-watermark-empty">
|
||||
<FileImageOutlined />
|
||||
@@ -5577,6 +5948,207 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</main>
|
||||
);
|
||||
|
||||
const translateLanguageOptions = [
|
||||
{ value: "zh", label: "中文" },
|
||||
{ value: "en", label: "English" },
|
||||
{ value: "ja", label: "日本語" },
|
||||
{ value: "ko", label: "한국어" },
|
||||
{ value: "fr", label: "Français" },
|
||||
{ value: "de", label: "Deutsch" },
|
||||
{ value: "es", label: "Español" },
|
||||
{ value: "pt", label: "Português" },
|
||||
{ value: "ru", label: "Русский" },
|
||||
{ value: "ar", label: "العربية" },
|
||||
];
|
||||
|
||||
const translatePreview = (
|
||||
<main key="translate" className="ecom-watermark-page ecom-translate-page ecom-tool-page-enter" aria-label="图片翻译">
|
||||
<input
|
||||
ref={translateInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="ecom-command-hidden-file"
|
||||
onChange={handleTranslateUpload}
|
||||
aria-label="上传翻译图片"
|
||||
/>
|
||||
<aside className="ecom-watermark-side">
|
||||
<header className="ecom-quick-set-panel-head ecom-watermark-panel-head">
|
||||
<strong className="ecom-quick-set-page-title">图片翻译</strong>
|
||||
<button type="button" className="ecom-quick-set-back" onClick={closeImageTranslatePage}>首页</button>
|
||||
<button type="button" className="ecom-quick-set-back" onClick={closeImageTranslatePage}>上一页</button>
|
||||
</header>
|
||||
<p className="ecom-watermark-intro">上传含文字的图片,AI 自动识别并翻译为目标语言。</p>
|
||||
|
||||
<section className="ecom-watermark-panel ecom-translate-lang-panel">
|
||||
<header>
|
||||
<strong>目标语言</strong>
|
||||
</header>
|
||||
<select
|
||||
className="ecom-translate-lang-select"
|
||||
value={translateLanguage}
|
||||
onChange={(event) => setTranslateLanguage(event.target.value)}
|
||||
aria-label="选择目标语言"
|
||||
>
|
||||
{translateLanguageOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</section>
|
||||
|
||||
<section className="ecom-watermark-panel">
|
||||
<header>
|
||||
<strong>上传素材</strong>
|
||||
<span>{translateImage ? "已上传" : "待上传"}</span>
|
||||
</header>
|
||||
<div
|
||||
className={`ecom-watermark-upload-card${isTranslateDragging ? " is-dragging" : ""}${translateImage ? " has-image" : ""}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => translateInputRef.current?.click()}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
translateInputRef.current?.click();
|
||||
}
|
||||
}}
|
||||
onDragEnter={(event) => {
|
||||
event.preventDefault();
|
||||
setIsTranslateDragging(true);
|
||||
}}
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
onDragLeave={() => setIsTranslateDragging(false)}
|
||||
onDrop={handleTranslateDrop}
|
||||
>
|
||||
{translateImage ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="ecom-watermark-remove"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
removeTranslateImage();
|
||||
}}
|
||||
aria-label="删除素材"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<figure>
|
||||
<img src={translateImage.src} alt={translateImage.name} />
|
||||
</figure>
|
||||
<div>
|
||||
<strong>{translateImage.name}</strong>
|
||||
<span>{translateImage.format || "PNG / JPG / WebP"}</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CloudUploadOutlined />
|
||||
<strong>上传含文字图片</strong>
|
||||
<span>支持 PNG / JPG / WebP,拖拽或点击上传</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="ecom-watermark-url-row">
|
||||
<input
|
||||
ref={translateUrlInputRef}
|
||||
placeholder="粘贴图片 URL"
|
||||
aria-label="粘贴图片 URL"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") void handleTranslateUrlImport();
|
||||
}}
|
||||
/>
|
||||
<button type="button" onClick={() => void handleTranslateUrlImport()}>导入</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="ecom-watermark-panel">
|
||||
<strong>处理说明</strong>
|
||||
<p>自动识别图片中的文字内容,翻译为目标语言并保持原图排版,适合商品包装、说明书、宣传图翻译。</p>
|
||||
</section>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="ecom-watermark-primary"
|
||||
onClick={handleTranslateGenerate}
|
||||
disabled={!translateImage || translateStatus === "processing"}
|
||||
>
|
||||
{translateStatus === "processing" ? <LoadingOutlined /> : <GlobalOutlined />}
|
||||
{translateStatus === "processing" ? "翻译中" : "开始翻译"}
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<section className="ecom-watermark-workspace">
|
||||
{!translateImage ? (
|
||||
<div
|
||||
className={`ecom-watermark-dropzone${isTranslateDragging ? " is-dragging" : ""}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => translateInputRef.current?.click()}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
translateInputRef.current?.click();
|
||||
}
|
||||
}}
|
||||
onDragEnter={(event) => {
|
||||
event.preventDefault();
|
||||
setIsTranslateDragging(true);
|
||||
}}
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
onDragLeave={() => setIsTranslateDragging(false)}
|
||||
onDrop={handleTranslateDrop}
|
||||
>
|
||||
<CloudUploadOutlined />
|
||||
<strong>点击或拖拽上传图片</strong>
|
||||
<span>支持 PNG / JPG / WebP,上传含文字图片后选择目标语言并点击开始翻译</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ecom-watermark-grid">
|
||||
<article className="ecom-watermark-preview-card">
|
||||
<span>原图</span>
|
||||
<img src={translateImage.src} alt="原图" />
|
||||
</article>
|
||||
|
||||
<article className="ecom-watermark-preview-card">
|
||||
<span>翻译结果</span>
|
||||
{translateStatus === "processing" ? (
|
||||
<div className="ecom-watermark-processing" role="status" aria-live="polite">
|
||||
<LoadingOutlined />
|
||||
<strong>正在翻译</strong>
|
||||
<em>AI 正在识别并翻译图片中的文字</em>
|
||||
</div>
|
||||
) : translateStatus === "done" ? (
|
||||
<>
|
||||
<img src={translateImage.src} alt="翻译结果" />
|
||||
<button type="button" className="ecom-watermark-zoom" aria-label="查看大图">
|
||||
<QuestionCircleOutlined />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="ecom-watermark-empty">
|
||||
<GlobalOutlined />
|
||||
<strong>等待处理</strong>
|
||||
<em>点击开始翻译后显示结果</em>
|
||||
</div>
|
||||
)}
|
||||
<div className="ecom-watermark-actions">
|
||||
<button type="button" onClick={() => toast.success("已加入资产库")} disabled={translateStatus !== "done"}>
|
||||
<FolderOpenOutlined />
|
||||
加入资产库
|
||||
</button>
|
||||
<button type="button" onClick={handleTranslateDownload} disabled={translateStatus !== "done"}>
|
||||
<CloudUploadOutlined />
|
||||
下载图片
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
|
||||
const openQuickUploadWithKeyboard = (
|
||||
event: ReactKeyboardEvent<HTMLDivElement>,
|
||||
inputRef: { current: HTMLInputElement | null },
|
||||
@@ -5738,6 +6310,38 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
{detailStatus === "done" && detailResultUrl ? (
|
||||
<section className="ecom-quick-detail-result" style={{ transform: `scale(${previewZoom})` }}>
|
||||
<img src={detailResultUrl} alt="A+详情页生成结果" />
|
||||
<button
|
||||
type="button"
|
||||
className="ecom-quick-detail-download"
|
||||
onClick={() => {
|
||||
const link = document.createElement("a");
|
||||
link.href = detailResultUrl;
|
||||
link.download = `A+详情页-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}}
|
||||
>
|
||||
<CloudUploadOutlined />
|
||||
保存本地
|
||||
</button>
|
||||
</section>
|
||||
) : detailStatus === "generating" ? (
|
||||
<section className="ecom-quick-set-generating">
|
||||
<LoadingOutlined />
|
||||
<strong>正在生成 A+ 详情页</strong>
|
||||
<span>AI 正在根据您的商品图和设置生成详情页素材,请稍候...</span>
|
||||
<div className="ecom-quick-set-progress">
|
||||
<div className="ecom-quick-set-progress-bar" style={{ width: `${Math.round(detailProgress)}%` }} />
|
||||
</div>
|
||||
<em className="ecom-quick-set-progress-text">{Math.round(detailProgress)}%</em>
|
||||
</section>
|
||||
) : detailStatus === "failed" ? (
|
||||
<section className="ecom-quick-set-failed">
|
||||
<FrownOutlined />
|
||||
<strong>生成失败</strong>
|
||||
<span>请检查网络或重试,如余额不足请先充值。</span>
|
||||
<button type="button" onClick={handleDetailGenerate} disabled={!canGenerateDetail}>重新生成</button>
|
||||
</section>
|
||||
) : detailProductImages.length ? (
|
||||
<section className="ecom-quick-detail-preview-card" style={{ transform: `scale(${previewZoom})` }}>
|
||||
@@ -5878,22 +6482,24 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
: isCloneTool
|
||||
? isWatermarkTool
|
||||
? watermarkPreview
|
||||
: isImageEditTool
|
||||
? imageWorkbenchPreview
|
||||
: isSmartCutoutTool
|
||||
? smartCutoutPreview
|
||||
: isQuickDetailTool
|
||||
? (
|
||||
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
||||
{quickDetailPreview}
|
||||
</div>
|
||||
)
|
||||
: clonePreview
|
||||
: isTranslateTool
|
||||
? translatePreview
|
||||
: isImageEditTool
|
||||
? imageWorkbenchPreview
|
||||
: isSmartCutoutTool
|
||||
? smartCutoutPreview
|
||||
: isQuickDetailTool
|
||||
? (
|
||||
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
||||
{quickDetailPreview}
|
||||
</div>
|
||||
)
|
||||
: clonePreview
|
||||
: placeholderPreview;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`product-clone-page page-motion${isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : ""}${isCloneTool && isCommandHistoryCollapsed ? " is-history-collapsed" : ""}${isSmartCutoutTool ? " is-smart-cutout-page" : ""}${isQuickDetailTool ? " is-quick-set-page" : ""}${isWatermarkTool ? " is-watermark-page" : ""}${isImageEditTool ? " is-image-workbench-page" : ""}`}
|
||||
className={`product-clone-page page-motion${isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : ""}${isCloneTool && isCommandHistoryCollapsed ? " is-history-collapsed" : ""}${isSmartCutoutTool ? " is-smart-cutout-page" : ""}${isQuickDetailTool ? " is-quick-set-page" : ""}${isWatermarkTool ? " is-watermark-page" : ""}${isTranslateTool ? " is-translate-page" : ""}${isImageEditTool ? " is-image-workbench-page" : ""}`}
|
||||
data-tool={activeTool}
|
||||
aria-label={pageLabel}
|
||||
>
|
||||
@@ -5917,7 +6523,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
{isSetTool ? setPanel : isDetail ? detailPanel : isTryOn ? tryOnPanel : isCloneTool ? clonePanel : placeholderPanel}
|
||||
</aside>
|
||||
|
||||
{isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isImageEditTool ? (
|
||||
{isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool ? (
|
||||
<button
|
||||
type="button"
|
||||
className="clone-ai-settings-toggle"
|
||||
@@ -6018,6 +6624,21 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</button>
|
||||
<img src={selectedProductSetPreview.src} alt={selectedProductSetPreview.label} />
|
||||
<strong>{selectedProductSetPreview.label}</strong>
|
||||
<button
|
||||
type="button"
|
||||
className="product-set-preview-download"
|
||||
onClick={() => {
|
||||
const link = document.createElement("a");
|
||||
link.href = selectedProductSetPreview.src;
|
||||
link.download = `${selectedProductSetPreview.label || "生成结果"}-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}}
|
||||
>
|
||||
<CloudUploadOutlined />
|
||||
保存本地
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user