diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index e3dac72..ee4df8a 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -81,12 +81,12 @@ const smartCutoutColorPresets = [ const smartCutoutSizeOptions = [ { key: "original", label: "原尺寸", icon: "image", frameWidth: "min(520px, 78%)", frameAspect: "auto", imageMaxWidth: "78%", imageMaxHeight: "310px" }, { key: "trim", label: "裁剪到边缘", icon: "crop", frameWidth: "min(420px, 70%)", frameAspect: "auto", imageMaxWidth: "92%", imageMaxHeight: "360px" }, - { key: "taobao-1-1", label: "淘宝1:1主图", icon: "shop", frameWidth: "min(430px, 72%)", frameAspect: "1 / 1", imageMaxWidth: "82%", imageMaxHeight: "82%" }, - { key: "taobao-3-4", label: "淘宝3:4主图", icon: "shop", frameWidth: "min(330px, 56%)", frameAspect: "3 / 4", imageMaxWidth: "82%", imageMaxHeight: "82%" }, - { key: "pdd-main", label: "拼多多主图", icon: "pdd", frameWidth: "min(430px, 72%)", frameAspect: "1 / 1", imageMaxWidth: "82%", imageMaxHeight: "82%" }, - { key: "xiaohongshu-cover", label: "小红书封面", icon: "text", frameWidth: "min(330px, 56%)", frameAspect: "3 / 4", imageMaxWidth: "82%", imageMaxHeight: "82%" }, - { key: "one-inch", label: "一寸头像", icon: "portrait", frameWidth: "min(290px, 50%)", frameAspect: "25 / 35", imageMaxWidth: "86%", imageMaxHeight: "86%" }, - { key: "two-inch", label: "二寸头像", icon: "portrait", frameWidth: "min(320px, 54%)", frameAspect: "35 / 49", imageMaxWidth: "86%", imageMaxHeight: "86%" }, + { key: "one-inch", label: "一寸头像", sizeLabel: "295*413", icon: "portrait", frameWidth: "min(290px, 50%)", frameAspect: "295 / 413", imageMaxWidth: "86%", imageMaxHeight: "86%", outputWidth: 295, outputHeight: 413 }, + { key: "two-inch", label: "二寸头像", sizeLabel: "413*579", icon: "portrait", frameWidth: "min(320px, 54%)", frameAspect: "413 / 579", imageMaxWidth: "86%", imageMaxHeight: "86%", outputWidth: 413, outputHeight: 579 }, + { key: "taobao-1-1", label: "淘宝1:1主图", sizeLabel: "800*800", icon: "shop", frameWidth: "min(430px, 72%)", frameAspect: "800 / 800", imageMaxWidth: "82%", imageMaxHeight: "82%", outputWidth: 800, outputHeight: 800 }, + { key: "taobao-3-4", label: "淘宝3:4主图", sizeLabel: "750*1000", icon: "shop", frameWidth: "min(330px, 56%)", frameAspect: "750 / 1000", imageMaxWidth: "82%", imageMaxHeight: "82%", outputWidth: 750, outputHeight: 1000 }, + { key: "pdd-main", label: "拼多多主图", sizeLabel: "800*800", icon: "pdd", frameWidth: "min(430px, 72%)", frameAspect: "800 / 800", imageMaxWidth: "82%", imageMaxHeight: "82%", outputWidth: 800, outputHeight: 800 }, + { key: "xiaohongshu-cover", label: "小红书封面", sizeLabel: "1242*1660", icon: "text", frameWidth: "min(330px, 56%)", frameAspect: "1242 / 1660", imageMaxWidth: "82%", imageMaxHeight: "82%", outputWidth: 1242, outputHeight: 1660 }, { key: "ratio-1-1", label: "1:1", icon: "square", frameWidth: "min(430px, 72%)", frameAspect: "1 / 1", imageMaxWidth: "82%", imageMaxHeight: "82%" }, { key: "ratio-3-2", label: "3:2", icon: "landscape", frameWidth: "min(520px, 78%)", frameAspect: "3 / 2", imageMaxWidth: "82%", imageMaxHeight: "82%" }, { key: "ratio-2-3", label: "2:3", icon: "portrait-ratio", frameWidth: "min(330px, 56%)", frameAspect: "2 / 3", imageMaxWidth: "82%", imageMaxHeight: "82%" }, @@ -97,6 +97,7 @@ const smartCutoutSizeOptions = [ ] as const; type SmartCutoutSizeKey = (typeof smartCutoutSizeOptions)[number]["key"]; +type SmartCutoutImageItem = { src: string; name: string; originalSrc?: string }; const clampNumber = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); @@ -1214,15 +1215,16 @@ 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" | "set" | "detail" | "watermark" | "image-edit" | null>(null); - const [smartCutoutImage, setSmartCutoutImage] = useState<{ src: string; name: string } | null>(null); - const [smartCutoutBatchImages, setSmartCutoutBatchImages] = useState<{ src: string; name: string }[]>([]); + const [activeQuickTool, setActiveQuickTool] = useState<"cutout" | "set" | "detail" | "watermark" | "image-edit" | "hot-video" | null>(null); + const [smartCutoutImage, setSmartCutoutImage] = useState(null); + const [smartCutoutBatchImages, setSmartCutoutBatchImages] = useState([]); const [smartCutoutBackgroundColor, setSmartCutoutBackgroundColor] = useState("#ffffff"); const [smartCutoutBackgroundAlpha, setSmartCutoutBackgroundAlpha] = useState(100); const [smartCutoutHexDraft, setSmartCutoutHexDraft] = useState("#ffffff"); const [isSmartCutoutPaletteOpen, setIsSmartCutoutPaletteOpen] = useState(false); const [smartCutoutSizeKey, setSmartCutoutSizeKey] = useState("original"); const [isSmartCutoutDragging, setIsSmartCutoutDragging] = useState(false); + const [isSmartCutoutComparing, setIsSmartCutoutComparing] = useState(false); const [isSmartCutoutTransitioning, setIsSmartCutoutTransitioning] = useState(false); const [smartCutoutTransitionMessage, setSmartCutoutTransitionMessage] = useState({ title: "正在切换页面", @@ -1656,8 +1658,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { if (files.length) void addSetImages(files); }; - const revokeSmartCutoutItems = (items: { src: string }[]) => { - items.forEach((item) => URL.revokeObjectURL(item.src)); + const revokeSmartCutoutItem = (item: SmartCutoutImageItem | null) => { + if (!item) return; + URL.revokeObjectURL(item.src); + if (item.originalSrc && item.originalSrc !== item.src) URL.revokeObjectURL(item.originalSrc); + }; + + const revokeSmartCutoutItems = (items: SmartCutoutImageItem[]) => { + items.forEach(revokeSmartCutoutItem); }; const clearSmartCutoutTransition = () => { @@ -1695,9 +1703,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { return []; }); setSmartCutoutImage((current) => { - if (current?.src) URL.revokeObjectURL(current.src); + revokeSmartCutoutItem(current); return null; }); + setIsSmartCutoutComparing(false); setComposerMenu(null); }; @@ -2050,6 +2059,21 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { if (!selectedDetailModules.length) setSelectedDetailModules(defaultCloneDetailModuleIds); }; + const openHotVideoPage = () => { + clearSmartCutoutTransition(); + setActiveQuickTool("hot-video"); + setComposerMenu(null); + setIsCloneSettingsCollapsed(true); + setIsCommandHistoryCollapsed(true); + }; + + const closeHotVideoPage = () => { + setActiveQuickTool(null); + setComposerMenu(null); + setIsCloneSettingsCollapsed(false); + setIsCommandHistoryCollapsed(false); + }; + const closeSmartCutoutTool = () => { runSmartCutoutPageTransition( { @@ -2062,9 +2086,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { return []; }); setSmartCutoutImage((current) => { - if (current?.src) URL.revokeObjectURL(current.src); + revokeSmartCutoutItem(current); return null; }); + setIsSmartCutoutComparing(false); setActiveQuickTool(null); setComposerMenu(null); }, @@ -2087,9 +2112,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { return []; }); setSmartCutoutImage((current) => { - if (current?.src) URL.revokeObjectURL(current.src); + revokeSmartCutoutItem(current); return null; }); + setIsSmartCutoutComparing(false); }, ); }; @@ -2106,10 +2132,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { return []; }); setSmartCutoutImage((current) => { - if (current?.src) URL.revokeObjectURL(current.src); + revokeSmartCutoutItem(current); return null; }); - const nextImages = imageFiles.map((file) => ({ src: URL.createObjectURL(file), name: file.name })); + setIsSmartCutoutComparing(false); + const nextImages = imageFiles.map((file) => { + const originalSrc = URL.createObjectURL(file); + return { src: originalSrc, originalSrc, name: file.name }; + }); smartCutoutPendingUrlsRef.current = nextImages.map((item) => item.src); setActiveQuickTool("cutout"); setSmartCutoutSizeKey("original"); @@ -2154,17 +2184,30 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { [smartCutoutSizeKey], ); + const previewSmartCutoutSize = isSmartCutoutComparing ? smartCutoutSizeOptions[0] : selectedSmartCutoutSize; + const previewSmartCutoutSizeKey = isSmartCutoutComparing ? "original" : smartCutoutSizeKey; + const previewSmartCutoutImageSrc = isSmartCutoutComparing ? smartCutoutImage?.originalSrc ?? smartCutoutImage?.src : smartCutoutImage?.src; + const smartCutoutFrameStyle = useMemo( () => ({ "--smart-cutout-bg": smartCutoutBackgroundValue, - "--smart-cutout-frame-width": selectedSmartCutoutSize.frameWidth, - "--smart-cutout-frame-aspect": selectedSmartCutoutSize.frameAspect, - "--smart-cutout-image-max-width": selectedSmartCutoutSize.imageMaxWidth, - "--smart-cutout-image-max-height": selectedSmartCutoutSize.imageMaxHeight, + "--smart-cutout-frame-width": previewSmartCutoutSize.frameWidth, + "--smart-cutout-frame-aspect": previewSmartCutoutSize.frameAspect, + "--smart-cutout-image-max-width": previewSmartCutoutSize.imageMaxWidth, + "--smart-cutout-image-max-height": previewSmartCutoutSize.imageMaxHeight, } as CSSProperties), - [selectedSmartCutoutSize, smartCutoutBackgroundValue], + [previewSmartCutoutSize, smartCutoutBackgroundValue], ); + const showSmartCutoutOriginalCompare = (event: ReactPointerEvent) => { + event.currentTarget.setPointerCapture(event.pointerId); + setIsSmartCutoutComparing(true); + }; + + const hideSmartCutoutOriginalCompare = () => { + setIsSmartCutoutComparing(false); + }; + const applySmartCutoutHsv = (h: number, s: number, v: number) => { const rgb = hsvToRgb(h, s, v); setSmartCutoutBackgroundColor(rgbToHex(rgb.r, rgb.g, rgb.b)); @@ -2221,9 +2264,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const aspect = parseSmartCutoutAspect(selectedSmartCutoutSize.frameAspect); const naturalWidth = Math.max(1, image.naturalWidth || image.width || 1200); const naturalHeight = Math.max(1, image.naturalHeight || image.height || 900); + const outputWidth = "outputWidth" in selectedSmartCutoutSize ? selectedSmartCutoutSize.outputWidth : undefined; + const outputHeight = "outputHeight" in selectedSmartCutoutSize ? selectedSmartCutoutSize.outputHeight : undefined; let canvasWidth = naturalWidth; let canvasHeight = naturalHeight; - if (aspect) { + if (outputWidth && outputHeight) { + canvasWidth = outputWidth; + canvasHeight = outputHeight; + } else if (aspect) { const longSide = 1600; if (aspect >= 1) { canvasWidth = longSide; @@ -3413,6 +3461,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const isQuickDetailTool = isCloneTool && activeQuickTool === "detail"; const isWatermarkTool = isCloneTool && activeQuickTool === "watermark"; const isImageEditTool = isCloneTool && activeQuickTool === "image-edit"; + const isHotVideoTool = isCloneTool && activeQuickTool === "hot-video"; const pageLabel = isSetTool ? "鍟嗗搧濂楀浘" : isDetail ? "A+/璇︽儏椤?" : isTryOn ? "AI鏈嶉グ绌挎埓" : activeToolMeta?.label || "鍟嗗搧宸ュ叿"; const setPrimaryLabel = setImages.length === 0 @@ -4464,7 +4513,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { { label: "变清晰", icon: }, { label: "AI消除", icon: }, { label: "证件照", icon: }, - { label: "爆款视频", icon: }, + { label: "爆款视频", icon: , onClick: openHotVideoPage }, { label: "拼图", icon: }, ].map((item) => ( - +
+ 功能
{smartCutoutSizeOptions.map((item) => ( - +
+ + + {item.label} + {"sizeLabel" in item ? {item.sizeLabel} : null} + +
))}
); + const hotVideoPreview = ( +
+ +
+