Codex/fix project review bugs #11

Merged
stringadmin merged 3 commits from codex/fix-project-review-bugs into main 2026-06-12 09:29:49 +00:00
4 changed files with 1145 additions and 150 deletions
Showing only changes of commit 7fdaa38504 - Show all commits
+2
View File
@@ -89,6 +89,8 @@ export interface ImageEditInput {
imageUrl: string;
function: string;
prompt?: string;
maskUrl?: string;
ratio?: string;
n?: number;
}
+696 -75
View File
@@ -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}
+414 -75
View File
@@ -4487,6 +4487,38 @@
pointer-events: none !important;
}
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page {
display: block !important;
height: 100% !important;
min-height: calc(100vh - 58px) !important;
overflow: hidden !important;
background: #f8f9fa !important;
}
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page .product-clone-shell {
display: block !important;
width: 100% !important;
height: 100% !important;
min-height: calc(100vh - 58px) !important;
padding: 0 !important;
}
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page .product-clone-rail,
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page .product-clone-panel,
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page .clone-ai-settings-toggle,
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page .ecom-command-history {
display: none !important;
}
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-translate-page .ecom-command-hidden-file {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-image-workbench-page .ecom-command-hidden-file {
position: absolute !important;
width: 1px !important;
@@ -4976,15 +5008,16 @@
.ecommerce-standalone .ecom-image-workbench-stage {
display: grid !important;
place-items: stretch !important;
min-width: 0 !important;
min-height: 0 !important;
height: 100% !important;
padding: 0 !important;
overflow: hidden !important;
border: 1px solid rgba(16, 115, 204, 0.14) !important;
border-radius: 14px !important;
background: #ffffff !important;
box-shadow: 0 14px 34px rgba(16, 115, 204, 0.08) !important;
border: none !important;
border-radius: 0 !important;
background: #f8f9fa !important;
box-shadow: none !important;
}
.ecommerce-standalone .ecom-image-workbench-canvas {
@@ -5117,21 +5150,41 @@
pointer-events: none !important;
}
.ecommerce-standalone .ecom-image-workbench-result {
display: grid !important;
place-items: center !important;
gap: 16px !important;
width: 100% !important;
}
.ecommerce-standalone .ecom-image-workbench-result img {
display: block !important;
max-width: 100% !important;
max-height: 60vh !important;
border-radius: 12px !important;
object-fit: contain !important;
}
.ecommerce-standalone .ecom-image-workbench-generating {
position: relative !important;
display: grid !important;
place-items: center !important;
width: 100% !important;
}
.ecommerce-standalone .ecom-watermark-page {
position: relative !important;
display: grid !important;
grid-template-columns: 350px minmax(0, 1fr) !important;
gap: 18px !important;
gap: 0 !important;
align-items: stretch !important;
width: 100% !important;
height: 100% !important;
min-height: calc(100vh - 58px) !important;
box-sizing: border-box !important;
padding: 18px !important;
padding: 0 !important;
color: #172636 !important;
background:
radial-gradient(circle at 54% 48%, rgba(30, 189, 219, 0.07), transparent 28rem),
#f8f9fa !important;
background: #f8f9fa !important;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif !important;
animation: ecom-smart-page-enter 440ms cubic-bezier(0.16, 1, 0.3, 1) both !important;
}
@@ -5175,26 +5228,25 @@
.ecommerce-standalone .ecom-watermark-side {
display: flex !important;
flex-direction: column !important;
gap: 12px !important;
gap: 10px !important;
height: 100% !important;
min-height: 0 !important;
padding: 18px 16px !important;
padding: 14px 16px !important;
overflow: auto !important;
border: 1px solid rgba(16, 115, 204, 0.14) !important;
border-radius: 14px !important;
background:
linear-gradient(180deg, rgba(16, 115, 204, 0.055), transparent 180px),
#ffffff !important;
box-shadow: 0 14px 34px rgba(16, 115, 204, 0.08) !important;
border: none !important;
border-right: 1px solid rgba(16, 115, 204, 0.1) !important;
border-radius: 0 !important;
background: #ffffff !important;
box-shadow: none !important;
}
.ecommerce-standalone .ecom-watermark-panel-head {
flex: 0 0 auto !important;
margin-bottom: 4px !important;
margin-bottom: 0 !important;
}
.ecommerce-standalone .ecom-watermark-intro {
margin: -2px 2px 2px !important;
margin: 0 2px 0 !important;
color: #66798a !important;
font-size: 12px !important;
font-weight: 750 !important;
@@ -5235,14 +5287,12 @@
.ecommerce-standalone .ecom-watermark-panel {
display: grid !important;
gap: 12px !important;
padding: 14px !important;
border: 1px solid rgba(16, 115, 204, 0.14) !important;
gap: 10px !important;
padding: 12px !important;
border: 1px solid rgba(16, 115, 204, 0.1) !important;
border-radius: 12px !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.5), transparent),
#ffffff !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75), 0 10px 28px rgba(16, 115, 204, 0.035) !important;
background: rgba(248, 252, 255, 0.6) !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75) !important;
}
.ecommerce-standalone .ecom-watermark-panel header {
@@ -5270,12 +5320,12 @@
.ecommerce-standalone .ecom-watermark-upload-card {
position: relative !important;
display: grid !important;
grid-template-columns: 74px minmax(0, 1fr) !important;
grid-template-columns: 68px minmax(0, 1fr) !important;
align-items: center !important;
gap: 12px !important;
min-height: 104px !important;
padding: 12px !important;
border: 1px dashed rgba(30, 189, 219, 0.5) !important;
min-height: 92px !important;
padding: 10px 12px !important;
border: 1px dashed rgba(30, 189, 219, 0.45) !important;
border-radius: 12px !important;
color: #607485 !important;
background: #fbfdff !important;
@@ -5330,8 +5380,8 @@
}
.ecommerce-standalone .ecom-watermark-upload-card figure {
width: 74px !important;
height: 74px !important;
width: 68px !important;
height: 68px !important;
margin: 0 !important;
overflow: hidden !important;
border: 1px solid rgba(16, 115, 204, 0.12) !important;
@@ -5395,16 +5445,23 @@
align-items: center !important;
justify-content: center !important;
gap: 9px !important;
min-height: 48px !important;
min-height: 44px !important;
width: 100% !important;
margin-top: 2px !important;
margin-top: auto !important;
border: 0 !important;
border-radius: 13px !important;
border-radius: 12px !important;
color: #ffffff !important;
background: linear-gradient(135deg, #1073cc, #1ebddb) !important;
box-shadow: 0 18px 38px rgba(16, 115, 204, 0.24) !important;
font-size: 15px !important;
box-shadow: 0 12px 28px rgba(16, 115, 204, 0.2) !important;
font-size: 14px !important;
font-weight: 950 !important;
cursor: pointer !important;
transition: box-shadow 180ms ease, transform 180ms ease !important;
}
.ecommerce-standalone .ecom-watermark-primary:hover:not(:disabled) {
box-shadow: 0 16px 36px rgba(16, 115, 204, 0.28) !important;
transform: translateY(-1px) !important;
}
.ecommerce-standalone .ecom-watermark-primary:disabled {
@@ -5421,10 +5478,10 @@
height: 100% !important;
padding: 0 !important;
overflow: hidden !important;
border: 1px solid rgba(16, 115, 204, 0.14) !important;
border-radius: 14px !important;
background: #ffffff !important;
box-shadow: 0 14px 34px rgba(16, 115, 204, 0.08) !important;
border: none !important;
border-radius: 0 !important;
background: #f8f9fa !important;
box-shadow: none !important;
}
.ecommerce-standalone .ecom-watermark-dropzone {
@@ -5531,6 +5588,54 @@
box-shadow: 0 12px 30px rgba(16, 115, 204, 0.1) !important;
}
.ecommerce-standalone .ecom-watermark-preview-card .ecom-image-workbench-image-frame {
position: relative !important;
display: block !important;
width: fit-content !important;
max-width: 100% !important;
max-height: 70vh !important;
overflow: hidden !important;
border-radius: 10px !important;
box-shadow: 0 12px 30px rgba(16, 115, 204, 0.1) !important;
cursor: crosshair !important;
touch-action: none !important;
user-select: none !important;
line-height: 0 !important;
}
.ecommerce-standalone .ecom-watermark-preview-card .ecom-image-workbench-image-frame img {
display: block !important;
max-width: 100% !important;
max-height: 70vh !important;
object-fit: contain !important;
border-radius: 10px !important;
user-select: none !important;
pointer-events: none !important;
-webkit-user-drag: none !important;
}
.ecommerce-standalone .ecom-watermark-preview-card .ecom-image-workbench-mask-layer {
position: absolute !important;
inset: 0 !important;
z-index: 3 !important;
width: 100% !important;
height: 100% !important;
overflow: hidden !important;
border-radius: 10px !important;
pointer-events: none !important;
}
.ecommerce-standalone .ecom-watermark-preview-card .ecom-image-workbench-brush {
position: absolute !important;
z-index: 4 !important;
border: 2px solid rgba(30, 189, 219, 0.72) !important;
border-radius: 50% !important;
background: rgba(30, 189, 219, 0.16) !important;
box-shadow: 0 0 0 6px rgba(30, 189, 219, 0.08) !important;
transform: translate(-50%, -50%) !important;
pointer-events: none !important;
}
.ecommerce-standalone .ecom-watermark-empty,
.ecommerce-standalone .ecom-watermark-processing {
display: grid !important;
@@ -5604,6 +5709,40 @@
transform: none !important;
}
.ecommerce-standalone .ecom-translate-lang-panel header {
margin-bottom: 8px !important;
}
.ecommerce-standalone .ecom-translate-lang-select {
display: block !important;
width: 100% !important;
height: 38px !important;
padding: 0 12px !important;
border: 1px solid #e0e6ed !important;
border-radius: 10px !important;
color: #172636 !important;
background: #ffffff !important;
font-size: 13px !important;
font-weight: 600 !important;
font-family: inherit !important;
cursor: pointer !important;
transition: border-color 180ms ease, box-shadow 180ms ease !important;
appearance: none !important;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 5l3 3 3-3' fill='none' stroke='%23596775' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
background-repeat: no-repeat !important;
background-position: right 12px center !important;
}
.ecommerce-standalone .ecom-translate-lang-select:focus {
border-color: #1073cc !important;
box-shadow: 0 0 0 3px rgba(16, 115, 204, 0.1) !important;
outline: none !important;
}
.ecommerce-standalone .ecom-translate-lang-select:hover {
border-color: #1073cc !important;
}
.ecommerce-standalone .ecom-quick-set-page {
position: relative !important;
display: grid !important;
@@ -6887,6 +7026,120 @@
font-weight: 950 !important;
}
.ecommerce-standalone .ecom-quick-set-generating,
.ecommerce-standalone .ecom-quick-set-failed {
display: grid !important;
place-items: center !important;
gap: 12px !important;
width: min(480px, 76%) !important;
min-height: 200px !important;
padding: 28px !important;
border: 1px solid rgba(16, 115, 204, 0.1) !important;
border-radius: 18px !important;
color: #738392 !important;
background: #ffffff !important;
box-shadow: 0 18px 48px rgba(16, 115, 204, 0.06) !important;
text-align: center !important;
}
.ecommerce-standalone .ecom-quick-set-generating .anticon {
display: inline-grid !important;
place-items: center !important;
width: 58px !important;
height: 58px !important;
border-radius: 50% !important;
color: #1073cc !important;
background: #edf8ff !important;
font-size: 26px !important;
animation: spin 1s linear infinite !important;
}
.ecommerce-standalone .ecom-quick-set-generating strong {
color: #172636 !important;
font-size: 19px !important;
font-weight: 950 !important;
}
.ecommerce-standalone .ecom-quick-set-generating span {
color: #738392 !important;
font-size: 13px !important;
line-height: 1.5 !important;
}
.ecommerce-standalone .ecom-quick-set-progress {
width: 100% !important;
max-width: 320px !important;
height: 6px !important;
border-radius: 3px !important;
background: #e8eef4 !important;
overflow: hidden !important;
}
.ecommerce-standalone .ecom-quick-set-progress-bar {
height: 100% !important;
border-radius: 3px !important;
background: linear-gradient(90deg, #1073cc, #38bdf8) !important;
transition: width 500ms ease !important;
}
.ecommerce-standalone .ecom-quick-set-progress-text {
color: #1073cc !important;
font-size: 13px !important;
font-weight: 700 !important;
font-style: normal !important;
}
.ecommerce-standalone .ecom-quick-set-failed .anticon {
display: inline-grid !important;
place-items: center !important;
width: 58px !important;
height: 58px !important;
border-radius: 50% !important;
color: #e04545 !important;
background: #fff0f0 !important;
font-size: 26px !important;
}
.ecommerce-standalone .ecom-quick-set-failed strong {
color: #172636 !important;
font-size: 19px !important;
font-weight: 950 !important;
}
.ecommerce-standalone .ecom-quick-set-failed span {
color: #738392 !important;
font-size: 13px !important;
line-height: 1.5 !important;
}
.ecommerce-standalone .ecom-quick-set-failed button {
min-height: 36px !important;
padding: 0 20px !important;
border: 1px solid rgba(16, 115, 204, 0.14) !important;
border-radius: 10px !important;
color: #1073cc !important;
background: #ffffff !important;
font-size: 13px !important;
font-weight: 700 !important;
cursor: pointer !important;
transition: background 180ms ease, color 180ms ease !important;
}
.ecommerce-standalone .ecom-quick-set-failed button:hover:not(:disabled) {
color: #ffffff !important;
background: #1073cc !important;
}
.ecommerce-standalone .ecom-quick-set-failed button:disabled {
opacity: 0.45 !important;
cursor: not-allowed !important;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.ecommerce-standalone .ecom-quick-set-result-card {
display: grid !important;
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr) !important;
@@ -6993,6 +7246,68 @@
transform-origin: center !important;
}
.ecommerce-standalone .ecom-quick-detail-download {
position: absolute !important;
bottom: 16px !important;
right: 16px !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 7px !important;
min-height: 36px !important;
padding: 0 16px !important;
border: 1px solid rgba(16, 115, 204, 0.14) !important;
border-radius: 10px !important;
color: #1073cc !important;
background: rgba(255, 255, 255, 0.92) !important;
backdrop-filter: blur(6px) !important;
box-shadow: 0 8px 20px rgba(16, 115, 204, 0.06) !important;
font-size: 13px !important;
font-weight: 850 !important;
cursor: pointer !important;
transition: transform 180ms ease, color 180ms ease, background 180ms ease, box-shadow 180ms ease !important;
}
.ecommerce-standalone .ecom-quick-detail-download:hover {
color: #ffffff !important;
background: #1073cc !important;
box-shadow: 0 12px 26px rgba(16, 115, 204, 0.18) !important;
transform: translateY(-1px) !important;
}
.ecommerce-standalone .ecom-quick-detail-download:active {
transform: scale(0.96) !important;
}
.ecommerce-standalone .product-set-preview-download {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
gap: 7px !important;
min-height: 38px !important;
padding: 0 20px !important;
border: 1px solid rgba(16, 115, 204, 0.14) !important;
border-radius: 10px !important;
color: #1073cc !important;
background: #ffffff !important;
box-shadow: 0 8px 20px rgba(16, 115, 204, 0.06) !important;
font-size: 14px !important;
font-weight: 700 !important;
cursor: pointer !important;
transition: transform 180ms ease, color 180ms ease, background 180ms ease, box-shadow 180ms ease !important;
}
.ecommerce-standalone .product-set-preview-download:hover {
color: #ffffff !important;
background: #1073cc !important;
box-shadow: 0 12px 26px rgba(16, 115, 204, 0.18) !important;
transform: translateY(-1px) !important;
}
.ecommerce-standalone .product-set-preview-download:active {
transform: scale(0.96) !important;
}
.ecommerce-standalone .ecom-quick-set-prompt {
position: relative !important;
display: grid !important;
@@ -10841,21 +11156,17 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board {
position: relative !important;
display: grid !important;
grid-template-columns: 1.12fr 1.08fr 1fr 0.96fr 0.96fr !important;
gap: 12px !important;
min-height: 112px !important;
padding: 12px !important;
grid-template-columns: repeat(5, 1fr) !important;
gap: 10px !important;
min-height: 0 !important;
padding: 0 !important;
overflow: visible !important;
border: 0 !important;
border-radius: 22px !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(248, 253, 254, 0.42)),
rgba(255, 255, 255, 0.46) !important;
box-shadow:
0 20px 52px rgba(16, 115, 204, 0.055),
inset 0 1px 0 rgba(255, 255, 255, 0.78) !important;
backdrop-filter: blur(16px) saturate(1.08) !important;
-webkit-backdrop-filter: blur(16px) saturate(1.08) !important;
border-radius: 0 !important;
background: transparent !important;
box-shadow: none !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board::before {
@@ -10863,11 +11174,11 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board button {
min-height: 84px !important;
padding: 0 clamp(12px, 1.35vw, 18px) !important;
gap: 10px !important;
min-height: 72px !important;
padding: 12px 10px !important;
gap: 8px !important;
border: 0 !important;
border-radius: 18px !important;
border-radius: 16px !important;
background:
radial-gradient(circle at 16% 18%, color-mix(in srgb, var(--quick-accent) 18%, transparent), transparent 38%),
linear-gradient(135deg, rgba(255, 255, 255, 0.94), var(--quick-bg)) !important;
@@ -10899,10 +11210,10 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board button > span {
width: 38px !important;
height: 38px !important;
width: 34px !important;
height: 34px !important;
border: 0 !important;
border-radius: 14px !important;
border-radius: 12px !important;
background:
linear-gradient(180deg, color-mix(in srgb, var(--quick-accent) 18%, #ffffff), color-mix(in srgb, var(--quick-accent) 8%, #ffffff)) !important;
color: var(--quick-accent) !important;
@@ -10940,6 +11251,12 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
--quick-text: #542234;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board .ecom-command-quick-card--translate {
--quick-accent: #0891b2;
--quick-bg: #ecfeff;
--quick-text: #164e63;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board,
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board button,
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-quick-board button > span,
@@ -11616,24 +11933,20 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .clone-ai-input-wrapper.ecom-command-composer,
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .ecom-command-quick-board {
width: min(100%, 1024px) !important;
width: min(100%, 1088px) !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .ecom-command-quick-board {
width: min(100%, 820px) !important;
min-height: 0 !important;
grid-template-columns: repeat(4, minmax(142px, 1fr)) !important;
grid-template-columns: repeat(5, minmax(0, 1fr)) !important;
align-items: stretch !important;
gap: 10px !important;
margin-inline: auto !important;
padding: 8px !important;
border-radius: 18px !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.68), rgba(246, 252, 254, 0.42)),
rgba(255, 255, 255, 0.4) !important;
box-shadow:
0 14px 34px rgba(16, 115, 204, 0.045),
inset 0 1px 0 rgba(255, 255, 255, 0.72) !important;
padding: 0 !important;
border-radius: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .ecom-command-quick-board button {
@@ -11658,8 +11971,8 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
@media (max-width: 760px) {
html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[data-tool="clone"][data-tool="clone"] .ecom-command-composer-wrap.ecom-command-composer-wrap:has(.ecom-inspiration-lab) .ecom-command-quick-board {
width: min(100%, 420px) !important;
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
width: min(100%, 480px) !important;
grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
}
}
@@ -11789,9 +12102,21 @@ html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="c
overflow: hidden !important;
}
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-translate-page {
--ecom-history-offset: 0px !important;
--ecom-history-panel-width: 0px !important;
box-sizing: border-box !important;
width: 100% !important;
height: 100% !important;
min-height: 0 !important;
padding-right: 0 !important;
overflow: hidden !important;
}
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-smart-cutout-page > .product-clone-shell,
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-quick-set-page > .product-clone-shell,
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-watermark-page > .product-clone-shell {
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-watermark-page > .product-clone-shell,
html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="clone"].is-translate-page > .product-clone-shell {
box-sizing: border-box !important;
width: 100% !important;
max-width: none !important;
@@ -11800,6 +12125,7 @@ html body #root .ecommerce-standalone.web-shell .product-clone-page[data-tool="c
html body #root .ecommerce-standalone.web-shell .ecom-smart-cutout-page,
html body #root .ecommerce-standalone.web-shell .ecom-watermark-page,
html body #root .ecommerce-standalone.web-shell .ecom-translate-page,
html body #root .ecommerce-standalone.web-shell .ecom-quick-set-page {
box-sizing: border-box !important;
width: 100% !important;
@@ -11869,7 +12195,20 @@ html body #root .ecommerce-standalone.web-shell .ecom-quick-set-canvas {
html body #root .ecommerce-standalone.web-shell .ecom-watermark-page {
grid-template-columns: minmax(320px, 350px) minmax(0, 1fr) !important;
padding-top: 58px !important;
padding-top: 0 !important;
padding-left: 0 !important;
padding-right: 0 !important;
padding-bottom: 0 !important;
gap: 0 !important;
}
html body #root .ecommerce-standalone.web-shell .ecom-translate-page {
grid-template-columns: minmax(320px, 350px) minmax(0, 1fr) !important;
padding-top: 0 !important;
padding-left: 0 !important;
padding-right: 0 !important;
padding-bottom: 0 !important;
gap: 0 !important;
}
html body #root .ecommerce-standalone.web-shell .ecom-watermark-side,
+33
View File
@@ -7868,6 +7868,39 @@
transform: scale(0.94);
}
.product-set-preview-download {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
min-height: 38px;
padding: 0 20px;
border: 1px solid rgba(16, 115, 204, 0.14);
border-radius: 10px;
color: #1073cc;
background: #ffffff;
box-shadow: 0 8px 20px rgba(16, 115, 204, 0.06);
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition:
transform 180ms ease,
color 180ms ease,
background 180ms ease,
box-shadow 180ms ease;
}
.product-set-preview-download:hover {
color: #ffffff;
background: #1073cc;
box-shadow: 0 12px 26px rgba(16, 115, 204, 0.18);
transform: translateY(-1px);
}
.product-set-preview-download:active {
transform: scale(0.96);
}
@keyframes product-set-arrow-pulse {
0%,
100% {