From 7fdaa385049f24175e15653ab17d1e3ed944c9fe Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Fri, 12 Jun 2026 16:00:43 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E7=94=B5=E5=95=86=E5=BF=AB?= =?UTF-8?q?=E6=8D=B7=E5=B7=A5=E5=85=B7=E6=8E=A5=E5=85=A5=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?API=E5=B9=B6=E5=A2=9E=E5=BC=BA=E9=A2=84=E8=A7=88=E4=BA=A4?= =?UTF-8?q?=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 图片修改接入局部重绘API,改为左右对比布局 - 去水印接入真实API,带进度条 - A+详情页预览区增加生成中/失败状态与进度条 - 新增图片翻译页面(含语言选择器) - 快捷功能栏改为一行五列均分布局,移除白框 - 预览弹窗与A+详情页结果增加保存本地按钮 --- src/api/aiGenerationClient.ts | 2 + src/features/ecommerce/EcommercePage.tsx | 771 ++++++++++++++++++++--- src/styles/ecommerce-standalone.css | 489 +++++++++++--- src/styles/pages/ecommerce.css | 33 + 4 files changed, 1145 insertions(+), 150 deletions(-) diff --git a/src/api/aiGenerationClient.ts b/src/api/aiGenerationClient.ts index a69c91c..ec74c1a 100644 --- a/src/api/aiGenerationClient.ts +++ b/src/api/aiGenerationClient.ts @@ -89,6 +89,8 @@ export interface ImageEditInput { imageUrl: string; function: string; prompt?: string; + maskUrl?: string; + ratio?: string; n?: number; } diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index fbed900..812db3b 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -1333,9 +1333,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const smartCutoutInputRef = useRef(null); const imageWorkbenchInputRef = useRef(null); const imageWorkbenchUrlInputRef = useRef(null); + const imageWorkbenchProgressRef = useRef(null); const watermarkInputRef = useRef(null); const watermarkUrlInputRef = useRef(null); const watermarkProcessTimeoutRef = useRef(null); + const translateInputRef = useRef(null); + const translateUrlInputRef = useRef(null); + const translateProcessTimeoutRef = useRef(null); const smartCutoutTransitionTimeoutRef = useRef(null); const smartCutoutPendingUrlsRef = useRef([]); const smartCutoutPaletteRef = useRef(null); @@ -1345,6 +1349,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const commandComposerWrapRef = useRef(null); const garmentInputRef = useRef(null); const detailInputRef = useRef(null); + const detailProgressRef = useRef(null); const countHoldTimeoutRef = useRef(null); const countHoldIntervalRef = useRef(null); const isAuthenticated = Boolean((_props as Record).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([]); - 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(null); const [smartCutoutBatchImages, setSmartCutoutBatchImages] = useState([]); 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(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 }>>([]); const [imageWorkbenchBrushCursor, setImageWorkbenchBrushCursor] = useState<{ x: number; y: number } | null>(null); + const [imageWorkbenchResultUrl, setImageWorkbenchResultUrl] = useState(null); + const [imageWorkbenchProgress, setImageWorkbenchProgress] = useState(0); const [isProductUploadDragging, setIsProductUploadDragging] = useState(false); const [cloneOutput, setCloneOutput] = useState(defaultCloneOutput); const [videoHistoryVisible, setVideoHistoryVisible] = useState(false); @@ -1699,6 +1712,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const [selectedDetailModules, setSelectedDetailModules] = useState(defaultDetailModuleIds); const [detailStatus, setDetailStatus] = useState("idle"); const [detailResultUrl, setDetailResultUrl] = useState(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) => { + const file = event.target.files?.[0]; + if (!file) return; + addTranslateImage(file); + event.target.value = ""; + }; + + const handleTranslateDrop = (event: DragEvent) => { + 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: , onClick: openImageWorkbenchPage }, { label: "智能抠图", tone: "cutout", icon: , onClick: openSmartCutoutUpload }, { label: "去除水印", tone: "watermark", icon: , onClick: openWatermarkRemovalPage }, + { label: "图片翻译", tone: "translate", icon: , onClick: openImageTranslatePage }, ].map((item) => ( + + + + + )} ); @@ -5545,14 +5906,24 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { 正在去水印 AI 正在清理图片中的水印和文字 +
+
+
+ {Math.round(watermarkProgress)}%
- ) : watermarkStatus === "done" ? ( + ) : watermarkStatus === "done" && watermarkResultUrl ? ( <> - 去水印结果 + 去水印结果 + ) : watermarkStatus === "failed" ? ( +
+ + 去水印失败 + 请检查网络或重试,如余额不足请先充值 +
) : (
@@ -5577,6 +5948,207 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { ); + 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 = ( +
+ + + +
+ {!translateImage ? ( +
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} + > + + 点击或拖拽上传图片 + 支持 PNG / JPG / WebP,上传含文字图片后选择目标语言并点击开始翻译 +
+ ) : ( +
+
+ 原图 + 原图 +
+ +
+ 翻译结果 + {translateStatus === "processing" ? ( +
+ + 正在翻译 + AI 正在识别并翻译图片中的文字 +
+ ) : translateStatus === "done" ? ( + <> + 翻译结果 + + + ) : ( +
+ + 等待处理 + 点击开始翻译后显示结果 +
+ )} +
+ + +
+
+
+ )} +
+
+ ); + const openQuickUploadWithKeyboard = ( event: ReactKeyboardEvent, inputRef: { current: HTMLInputElement | null }, @@ -5738,6 +6310,38 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { {detailStatus === "done" && detailResultUrl ? (
A+详情页生成结果 + +
+ ) : detailStatus === "generating" ? ( +
+ + 正在生成 A+ 详情页 + AI 正在根据您的商品图和设置生成详情页素材,请稍候... +
+
+
+ {Math.round(detailProgress)}% +
+ ) : detailStatus === "failed" ? ( +
+ + 生成失败 + 请检查网络或重试,如余额不足请先充值。 +
) : detailProductImages.length ? (
@@ -5878,22 +6482,24 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { : isCloneTool ? isWatermarkTool ? watermarkPreview - : isImageEditTool - ? imageWorkbenchPreview - : isSmartCutoutTool - ? smartCutoutPreview - : isQuickDetailTool - ? ( -
- {quickDetailPreview} -
- ) - : clonePreview + : isTranslateTool + ? translatePreview + : isImageEditTool + ? imageWorkbenchPreview + : isSmartCutoutTool + ? smartCutoutPreview + : isQuickDetailTool + ? ( +
+ {quickDetailPreview} +
+ ) + : clonePreview : placeholderPreview; return (
@@ -5917,7 +6523,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { {isSetTool ? setPanel : isDetail ? detailPanel : isTryOn ? tryOnPanel : isCloneTool ? clonePanel : placeholderPanel} - {isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isImageEditTool ? ( + {isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool ? (
) : null} diff --git a/src/styles/ecommerce-standalone.css b/src/styles/ecommerce-standalone.css index 9c9e0ed..f04f893 100644 --- a/src/styles/ecommerce-standalone.css +++ b/src/styles/ecommerce-standalone.css @@ -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, diff --git a/src/styles/pages/ecommerce.css b/src/styles/pages/ecommerce.css index 780db21..dd47c23 100644 --- a/src/styles/pages/ecommerce.css +++ b/src/styles/pages/ecommerce.css @@ -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% { -- 2.52.0 From ad4bca31b134454877b1285834188dee2ec4a44a Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Fri, 12 Jun 2026 17:25:30 +0800 Subject: [PATCH 2/2] fix: address project review bugs --- .gitignore | 1 + src/api/adVideoPlanClient.ts | 165 ++++++++++-------- src/api/aiGenerationClient.ts | 27 ++- src/api/serverConnection.ts | 4 + src/api/webGenerationGateway.ts | 1 - src/features/ecommerce/EcommercePage.tsx | 12 +- .../ecommerce/EcommerceVideoWorkspace.tsx | 56 +++++- .../ecommerce/ecommerceVideoService.ts | 21 ++- src/styles/ecommerce-standalone.css | 18 +- src/utils/errorReporting.ts | 6 +- vite.config.ts | 22 ++- 11 files changed, 209 insertions(+), 124 deletions(-) diff --git a/.gitignore b/.gitignore index ddc64a0..65b2764 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ tmp/ *.swp *.swo coverage/ +屏幕截图 *.png diff --git a/src/api/adVideoPlanClient.ts b/src/api/adVideoPlanClient.ts index 1b245fc..3c5a608 100644 --- a/src/api/adVideoPlanClient.ts +++ b/src/api/adVideoPlanClient.ts @@ -1,8 +1,5 @@ import { buildApiUrl, buildAuthHeaders } from "./serverConnection"; -const TEXT_MODELS = ["qwen-max", "qwen-plus", "qwen-turbo"]; -const VISION_MODELS = ["qwen3.7-plus", "qwen-vl-plus", "qwen-vl-max"]; - type AbortSignalConstructorWithAny = typeof AbortSignal & { any?: (signals: AbortSignal[]) => AbortSignal; }; @@ -110,11 +107,45 @@ export interface ComplianceCheck { allow_video_generation: boolean; } +function findJsonSlice(raw: string): string { + const start = raw.search(/[\[{]/); + if (start < 0) return raw; + + const stack: string[] = []; + let inString = false; + let escaped = false; + + for (let index = start; index < raw.length; index += 1) { + const char = raw[index]; + + if (inString) { + if (escaped) { + escaped = false; + } else if (char === "\\") { + escaped = true; + } else if (char === "\"") { + inString = false; + } + continue; + } + + if (char === "\"") { + inString = true; + } else if (char === "{" || char === "[") { + stack.push(char === "{" ? "}" : "]"); + } else if (char === "}" || char === "]") { + if (stack.pop() !== char) break; + if (stack.length === 0) return raw.slice(start, index + 1); + } + } + + return raw.slice(start); +} + function extractJson(text: string): unknown { const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/); const raw = fenced ? fenced[1].trim() : text.trim(); - const start = raw.search(/[[{]/); - const slice = start >= 0 ? raw.slice(start) : raw; + const slice = findJsonSlice(raw); try { return JSON.parse(slice); } catch { @@ -122,9 +153,16 @@ function extractJson(text: string): unknown { } } +type ChatContent = + | string + | Array< + | { type: "image_url"; image_url: { url: string } } + | { type: "text"; text: string } + >; + interface ChatMessage { role: "system" | "user"; - content: string; + content: ChatContent; } const MAX_RETRIES = 3; @@ -171,43 +209,32 @@ async function chat( userContent: string, options?: { model?: string; signal?: AbortSignal }, ): Promise { - const candidateModels = options?.model ? [options.model] : TEXT_MODELS; - let lastError: Error | null = null; + return retryOnTransient(async () => { + const messages: ChatMessage[] = [ + { role: "system", content: systemPrompt }, + { role: "user", content: userContent }, + ]; + const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS); + const combinedSignal = combineAbortSignals(options?.signal, timeoutSignal); + const body: Record = { messages, stream: false, temperature: 0.4 }; + if (options?.model) body.model = options.model; - for (const model of candidateModels) { - try { - return await retryOnTransient(async () => { - const messages: ChatMessage[] = [ - { role: "system", content: systemPrompt }, - { role: "user", content: userContent }, - ]; - const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS); - const combinedSignal = combineAbortSignals(options?.signal, timeoutSignal); - const res = await fetch(buildApiUrl("ai/chat"), { - method: "POST", - headers: buildAuthHeaders(), - body: JSON.stringify({ model, messages, stream: false, temperature: 0.4 }), - signal: combinedSignal, - }); - if (!res.ok) { - const errBody = await res.text().catch(() => ""); - throw new Error(`AI 调用失败 (${res.status})${errBody ? `: ${errBody.slice(0, 120)}` : ""}`); - } - const payload = await res.json(); - const content: string = - payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? ""; - if (!content) throw new Error("模型未返回有效内容"); - return content; - }, options?.signal); - } catch (err) { - lastError = err instanceof Error ? err : new Error(String(err)); - if (options?.signal?.aborted) throw lastError; - // If user pinned a specific model, don't fall back to others - if (options?.model) throw lastError; - // Try next model in fallback chain + const res = await fetch(buildApiUrl("ai/chat"), { + method: "POST", + headers: buildAuthHeaders(), + body: JSON.stringify(body), + signal: combinedSignal, + }); + if (!res.ok) { + const errBody = await res.text().catch(() => ""); + throw new Error(`AI 调用失败 (${res.status})${errBody ? `: ${errBody.slice(0, 120)}` : ""}`); } - } - throw lastError ?? new Error("所有候选模型均不可用"); + const payload = await res.json(); + const content: string = + payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? ""; + if (!content) throw new Error("模型未返回有效内容"); + return content; + }, options?.signal); } async function visionChat( @@ -216,50 +243,36 @@ async function visionChat( imageUrls: string[], signal?: AbortSignal, ): Promise { - const content = [ - ...imageUrls.map((url) => ({ type: "image_url", image_url: { url } })), + const content: ChatContent = [ + ...imageUrls.map((url) => ({ type: "image_url" as const, image_url: { url } })), { type: "text", text }, ]; const messages = [ { role: "system", content: systemPrompt }, { role: "user", content }, - ]; + ] satisfies ChatMessage[]; - let lastError: Error | null = null; - for (const model of VISION_MODELS) { + return retryOnTransient(async () => { const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS); const combinedSignal = combineAbortSignals(signal, timeoutSignal); - try { - const out = await retryOnTransient(async () => { - const res = await fetch(buildApiUrl("ai/chat"), { - method: "POST", - headers: buildAuthHeaders(), - body: JSON.stringify({ model, messages, stream: false, temperature: 0.3 }), - signal: combinedSignal, - }); - if (!res.ok) { - const errBody = await res.text().catch(() => ""); - if (errBody.includes("image format")) throw new Error("IMAGE_FORMAT_FALLBACK"); - throw new Error(`图片理解调用失败 (${res.status})${errBody ? `: ${errBody.slice(0, 120)}` : ""}`); - } - const payload = await res.json(); - const result: string = - payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? ""; - if (!result) throw new Error("图片理解未返回有效内容"); - return result; - }, signal); - return out; - } catch (err) { - lastError = err instanceof Error ? err : new Error(String(err)); - if (signal?.aborted) throw lastError; - // Continue trying next vision model on transient failures, image format errors, or upstream errors - if (lastError.message === "IMAGE_FORMAT_FALLBACK") continue; - if (lastError.message.includes("图片理解调用失败")) continue; - if (isTransientError(lastError)) continue; - throw lastError; + + const res = await fetch(buildApiUrl("ai/chat"), { + method: "POST", + headers: buildAuthHeaders(), + body: JSON.stringify({ messages, stream: false, temperature: 0.3 }), + signal: combinedSignal, + }); + if (!res.ok) { + const errBody = await res.text().catch(() => ""); + if (errBody.includes("image format")) throw new Error("图片格式不受支持,请更换图片后重试"); + throw new Error(`图片理解调用失败 (${res.status})${errBody ? `: ${errBody.slice(0, 120)}` : ""}`); } - } - throw lastError ?? new Error("图片理解调用失败,所有模型均不可用"); + const payload = await res.json(); + const result: string = + payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? ""; + if (!result) throw new Error("图片理解未返回有效内容"); + return result; + }, signal); } const IMAGE_UNDERSTANDING_PROMPT = `你是电商产品图片分析专家。请分析用户提供的产品图片,识别产品主体、外观、颜色、材质、形状、尺寸感、品牌标识、关键部件、可视化卖点和适合展示的镜头角度。请用简洁的中文段落描述,不要编造图片中看不到的信息。`; diff --git a/src/api/aiGenerationClient.ts b/src/api/aiGenerationClient.ts index ec74c1a..db00e1f 100644 --- a/src/api/aiGenerationClient.ts +++ b/src/api/aiGenerationClient.ts @@ -12,7 +12,7 @@ import type { WebGenerationPreviewTask } from "../types"; export interface ImageGenInput { projectId?: string; conversationId?: number; - model: string; + model?: string; prompt: string; ratio?: string; quality?: string; @@ -210,18 +210,18 @@ function getStoredSessionRole(): string { } function emitImageRouteDebug(label: string, payload: Record): void { - // Only emit console logs for admin users — hides enterprise routing details - if (getStoredSessionRole() === "admin") { - const entry: ImageRouteDebugEntry = { - at: new Date().toISOString(), - label, - ...payload, - }; - try { - console.log(`${label} ${JSON.stringify(entry)}`); - } catch { - console.log(label, entry); - } + // Only emit route debug for admin users; provider routing is operational data. + if (getStoredSessionRole() !== "admin") return; + + const entry: ImageRouteDebugEntry = { + at: new Date().toISOString(), + label, + ...payload, + }; + try { + console.log(`${label} ${JSON.stringify(entry)}`); + } catch { + console.log(label, entry); } if (typeof window === "undefined") return; @@ -229,7 +229,6 @@ function emitImageRouteDebug(label: string, payload: Record): v const previousEntries = Array.isArray(debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__) ? debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__ : []; - const entry: ImageRouteDebugEntry = { at: new Date().toISOString(), label, ...payload }; debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__ = [...previousEntries.slice(-19), entry]; } diff --git a/src/api/serverConnection.ts b/src/api/serverConnection.ts index 4327da1..2316f72 100644 --- a/src/api/serverConnection.ts +++ b/src/api/serverConnection.ts @@ -161,7 +161,11 @@ export function clearAllUserStorage(): void { "omniai-web-profile-ui", "omniai:more-recent-tools", "omniai:generation-queue", + "omniai:generation-records.pending", + "omniai:ecommerce-video-workspace", "omniai-canvas-saved-assets", + "omniai.clone-ai.", + "omniai.ecommerce.", ]; for (let i = window.localStorage.length - 1; i >= 0; i--) { const key = window.localStorage.key(i); diff --git a/src/api/webGenerationGateway.ts b/src/api/webGenerationGateway.ts index 9a59609..53aed5b 100644 --- a/src/api/webGenerationGateway.ts +++ b/src/api/webGenerationGateway.ts @@ -50,7 +50,6 @@ export const webGenerationGateway = { const result = await aiGenerationClient.createImageTask({ projectId: params?.projectId, conversationId: params?.conversationId, - model: "gpt-image-2", prompt, ratio: params?.ratio || "16:9", quality: params?.quality || "1K", diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index e5e99b3..d1a549a 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -3453,8 +3453,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { return urls; }; - const IMAGE_MODEL = "gpt-image-2"; - const setCountLabels: Record = { selling: { label: "卖点图", promptDesc: "selling-point infographic image highlighting core product advantages and detail close-ups" }, white: { label: "白底图", promptDesc: "clean white-background product photo showing the item from its best angle, studio lighting, no props" }, @@ -3569,7 +3567,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const fullPrompt = userText.trim() ? `${subPrompt} Additional user requirements: ${userText.trim()}` : subPrompt; const { taskId } = await aiGenerationClient.createImageTask({ - model: IMAGE_MODEL, prompt: fullPrompt, ratio: normalizeRatioForApi(pRatio), quality: pRatio.includes("720") ? "720P" : "1080P", @@ -3653,7 +3650,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const stamp = Date.now(); const { taskId } = await aiGenerationClient.createImageTask({ - model: IMAGE_MODEL, prompt, ratio: normalizeRatioForApi(pRatio), quality: pRatio.includes("720") ? "720P" : "1080P", @@ -6564,6 +6560,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { + {isCloneTool && !isCommandHistoryCollapsed ? ( +
setIsCommandHistoryCollapsed(true)} + /> + ) : null} +