feat: refine ecommerce smart cutout editor
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
CloudUploadOutlined,
|
||||
CloseOutlined,
|
||||
FileImageOutlined,
|
||||
FolderOpenOutlined,
|
||||
FrownOutlined,
|
||||
GlobalOutlined,
|
||||
LoadingOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
@@ -11,8 +13,9 @@
|
||||
ReloadOutlined,
|
||||
SettingOutlined,
|
||||
SkinOutlined,
|
||||
TableOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useEffect, useMemo, useRef, useState, type CSSProperties, type ChangeEvent, type DragEvent, type MouseEvent as ReactMouseEvent, type ReactNode } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, type CSSProperties, type ChangeEvent, type DragEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import "../../styles/pages/ecommerce.css";
|
||||
import "../../styles/pages/local-theme-parity.css";
|
||||
import { ossAssets } from "../../data/ossAssets";
|
||||
@@ -35,6 +38,136 @@ import pinduoduoLogo from "../../assets/platform-logos/pinduoduo.webp";
|
||||
import shopeeLogo from "../../assets/platform-logos/shopee.webp";
|
||||
import taobaoLogo from "../../assets/platform-logos/taobao.webp";
|
||||
import tiktokShopLogo from "../../assets/platform-logos/tiktok-shop.webp";
|
||||
|
||||
const smartCutoutColorPresets = [
|
||||
"#ffffff",
|
||||
"#111111",
|
||||
"#ff3131",
|
||||
"#ff7a1a",
|
||||
"#f7c600",
|
||||
"#29b34a",
|
||||
"#25a9e0",
|
||||
"#438df5",
|
||||
"#9029d9",
|
||||
"#8aa3ad",
|
||||
"#6b7b86",
|
||||
"#f46f7b",
|
||||
"#ff9451",
|
||||
"#f7d34f",
|
||||
"#55c66f",
|
||||
"#73c7f3",
|
||||
"#6dabf5",
|
||||
"#b45adb",
|
||||
"#bcc8ce",
|
||||
"#aeb7bd",
|
||||
"#ffbec4",
|
||||
"#ffd1ac",
|
||||
"#f8e69d",
|
||||
"#91de9e",
|
||||
"#b7e5fb",
|
||||
"#b9d9fb",
|
||||
"#d7abe8",
|
||||
"#dfe5e8",
|
||||
"#d7dde0",
|
||||
"#ffe2e4",
|
||||
"#ffe5d1",
|
||||
"#f8efcf",
|
||||
"#c9efcf",
|
||||
"#d8f0fb",
|
||||
"#d8eafa",
|
||||
"#ead2f1",
|
||||
];
|
||||
|
||||
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: "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%" },
|
||||
{ key: "ratio-4-3", label: "4:3", icon: "landscape", frameWidth: "min(520px, 78%)", frameAspect: "4 / 3", imageMaxWidth: "82%", imageMaxHeight: "82%" },
|
||||
{ key: "ratio-3-4", label: "3:4", icon: "portrait-ratio", frameWidth: "min(330px, 56%)", frameAspect: "3 / 4", imageMaxWidth: "82%", imageMaxHeight: "82%" },
|
||||
{ key: "ratio-16-9", label: "16:9", icon: "wide", frameWidth: "min(560px, 82%)", frameAspect: "16 / 9", imageMaxWidth: "82%", imageMaxHeight: "82%" },
|
||||
{ key: "ratio-9-16", label: "9:16", icon: "phone", frameWidth: "min(260px, 46%)", frameAspect: "9 / 16", imageMaxWidth: "82%", imageMaxHeight: "82%" },
|
||||
] as const;
|
||||
|
||||
type SmartCutoutSizeKey = (typeof smartCutoutSizeOptions)[number]["key"];
|
||||
|
||||
const clampNumber = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
|
||||
|
||||
const normalizeHexColor = (value: string) => {
|
||||
const clean = value.trim().replace(/^#/, "");
|
||||
if (!/^[0-9a-fA-F]{6}$/.test(clean)) return null;
|
||||
return `#${clean.toLowerCase()}`;
|
||||
};
|
||||
|
||||
const hexToRgb = (value: string) => {
|
||||
const normalized = normalizeHexColor(value);
|
||||
if (!normalized) return null;
|
||||
const numeric = Number.parseInt(normalized.slice(1), 16);
|
||||
return {
|
||||
r: (numeric >> 16) & 255,
|
||||
g: (numeric >> 8) & 255,
|
||||
b: numeric & 255,
|
||||
};
|
||||
};
|
||||
|
||||
const rgbToHex = (r: number, g: number, b: number) =>
|
||||
`#${[r, g, b].map((item) => clampNumber(Math.round(item), 0, 255).toString(16).padStart(2, "0")).join("")}`;
|
||||
|
||||
const hsvToRgb = (h: number, s: number, v: number) => {
|
||||
const hue = ((h % 360) + 360) % 360;
|
||||
const saturation = clampNumber(s, 0, 100) / 100;
|
||||
const value = clampNumber(v, 0, 100) / 100;
|
||||
const chroma = value * saturation;
|
||||
const x = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));
|
||||
const match = value - chroma;
|
||||
const [red, green, blue] =
|
||||
hue < 60
|
||||
? [chroma, x, 0]
|
||||
: hue < 120
|
||||
? [x, chroma, 0]
|
||||
: hue < 180
|
||||
? [0, chroma, x]
|
||||
: hue < 240
|
||||
? [0, x, chroma]
|
||||
: hue < 300
|
||||
? [x, 0, chroma]
|
||||
: [chroma, 0, x];
|
||||
return {
|
||||
r: (red + match) * 255,
|
||||
g: (green + match) * 255,
|
||||
b: (blue + match) * 255,
|
||||
};
|
||||
};
|
||||
|
||||
const hexToHsv = (value: string) => {
|
||||
const rgb = hexToRgb(value) ?? { r: 255, g: 255, b: 255 };
|
||||
const red = rgb.r / 255;
|
||||
const green = rgb.g / 255;
|
||||
const blue = rgb.b / 255;
|
||||
const max = Math.max(red, green, blue);
|
||||
const min = Math.min(red, green, blue);
|
||||
const delta = max - min;
|
||||
const hue =
|
||||
delta === 0
|
||||
? 0
|
||||
: max === red
|
||||
? 60 * (((green - blue) / delta) % 6)
|
||||
: max === green
|
||||
? 60 * ((blue - red) / delta + 2)
|
||||
: 60 * ((red - green) / delta + 4);
|
||||
return {
|
||||
h: Math.round((hue + 360) % 360),
|
||||
s: max === 0 ? 0 : Math.round((delta / max) * 100),
|
||||
v: Math.round(max * 100),
|
||||
};
|
||||
};
|
||||
import tmallLogo from "../../assets/platform-logos/tmall.webp";
|
||||
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||
import { ServerRequestError } from "../../api/serverConnection";
|
||||
@@ -991,6 +1124,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const setInputRef = useRef<HTMLInputElement>(null);
|
||||
const productInputRef = useRef<HTMLInputElement>(null);
|
||||
const cloneReferenceInputRef = useRef<HTMLInputElement>(null);
|
||||
const smartCutoutInputRef = useRef<HTMLInputElement>(null);
|
||||
const smartCutoutTransitionTimeoutRef = useRef<number | null>(null);
|
||||
const smartCutoutPendingUrlsRef = useRef<string[]>([]);
|
||||
const smartCutoutPaletteRef = useRef<HTMLDivElement>(null);
|
||||
const smartCutoutToolsRef = useRef<HTMLDivElement>(null);
|
||||
const composerMenuCloseTimeoutRef = useRef<number | null>(null);
|
||||
const requirementTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const commandComposerWrapRef = useRef<HTMLElement | null>(null);
|
||||
const garmentInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -1025,12 +1164,28 @@ 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" | null>(null);
|
||||
const [smartCutoutImage, setSmartCutoutImage] = useState<{ src: string; name: string } | null>(null);
|
||||
const [smartCutoutBatchImages, setSmartCutoutBatchImages] = useState<{ src: string; name: string }[]>([]);
|
||||
const [smartCutoutBackgroundColor, setSmartCutoutBackgroundColor] = useState("#ffffff");
|
||||
const [smartCutoutBackgroundAlpha, setSmartCutoutBackgroundAlpha] = useState(100);
|
||||
const [smartCutoutHexDraft, setSmartCutoutHexDraft] = useState("#ffffff");
|
||||
const [isSmartCutoutPaletteOpen, setIsSmartCutoutPaletteOpen] = useState(false);
|
||||
const [smartCutoutSizeKey, setSmartCutoutSizeKey] = useState<SmartCutoutSizeKey>("original");
|
||||
const [isSmartCutoutDragging, setIsSmartCutoutDragging] = useState(false);
|
||||
const [isSmartCutoutTransitioning, setIsSmartCutoutTransitioning] = useState(false);
|
||||
const [smartCutoutTransitionMessage, setSmartCutoutTransitionMessage] = useState({
|
||||
title: "正在切换页面",
|
||||
subtitle: "请稍候",
|
||||
});
|
||||
const [isProductUploadDragging, setIsProductUploadDragging] = useState(false);
|
||||
const [cloneOutput, setCloneOutput] = useState<CloneOutputKey>(defaultCloneOutput);
|
||||
const [videoHistoryVisible, setVideoHistoryVisible] = useState(false);
|
||||
const [videoPlanTrigger, setVideoPlanTrigger] = useState(0);
|
||||
const [openCloneBasicSelect, setOpenCloneBasicSelect] = useState<CloneBasicSelectKey | null>(null);
|
||||
const [composerMenu, setComposerMenu] = useState<ComposerMenuKey | null>(null);
|
||||
const [visibleComposerMenu, setVisibleComposerMenu] = useState<ComposerMenuKey | null>(null);
|
||||
const [isComposerMenuClosing, setIsComposerMenuClosing] = useState(false);
|
||||
const [composerPopoverLeft, setComposerPopoverLeft] = useState(0);
|
||||
const [isCommandHistoryCollapsed, setIsCommandHistoryCollapsed] = useState(false);
|
||||
const [openCloneModelSelect, setOpenCloneModelSelect] = useState<CloneModelSelectKey | null>(null);
|
||||
@@ -1414,6 +1569,219 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
if (files.length) void addSetImages(files);
|
||||
};
|
||||
|
||||
const revokeSmartCutoutItems = (items: { src: string }[]) => {
|
||||
items.forEach((item) => URL.revokeObjectURL(item.src));
|
||||
};
|
||||
|
||||
const clearSmartCutoutTransition = () => {
|
||||
if (smartCutoutTransitionTimeoutRef.current !== null) {
|
||||
window.clearTimeout(smartCutoutTransitionTimeoutRef.current);
|
||||
smartCutoutTransitionTimeoutRef.current = null;
|
||||
}
|
||||
if (smartCutoutPendingUrlsRef.current.length) {
|
||||
smartCutoutPendingUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
|
||||
smartCutoutPendingUrlsRef.current = [];
|
||||
}
|
||||
setIsSmartCutoutTransitioning(false);
|
||||
};
|
||||
|
||||
const runSmartCutoutPageTransition = (message: { title: string; subtitle: string }, action: () => void, delay = 460) => {
|
||||
clearSmartCutoutTransition();
|
||||
setSmartCutoutTransitionMessage(message);
|
||||
setIsSmartCutoutTransitioning(true);
|
||||
smartCutoutTransitionTimeoutRef.current = window.setTimeout(() => {
|
||||
smartCutoutTransitionTimeoutRef.current = null;
|
||||
action();
|
||||
setIsSmartCutoutTransitioning(false);
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const openSmartCutoutUpload = () => {
|
||||
clearSmartCutoutTransition();
|
||||
setSmartCutoutTransitionMessage({
|
||||
title: "正在进入智能抠图",
|
||||
subtitle: "为你打开图片处理工具",
|
||||
});
|
||||
setActiveQuickTool("cutout");
|
||||
setSmartCutoutBatchImages((current) => {
|
||||
revokeSmartCutoutItems(current);
|
||||
return [];
|
||||
});
|
||||
setSmartCutoutImage((current) => {
|
||||
if (current?.src) URL.revokeObjectURL(current.src);
|
||||
return null;
|
||||
});
|
||||
setComposerMenu(null);
|
||||
};
|
||||
|
||||
const closeSmartCutoutTool = () => {
|
||||
runSmartCutoutPageTransition(
|
||||
{
|
||||
title: "正在返回首页",
|
||||
subtitle: "回到电商智能体",
|
||||
},
|
||||
() => {
|
||||
setSmartCutoutBatchImages((current) => {
|
||||
revokeSmartCutoutItems(current);
|
||||
return [];
|
||||
});
|
||||
setSmartCutoutImage((current) => {
|
||||
if (current?.src) URL.revokeObjectURL(current.src);
|
||||
return null;
|
||||
});
|
||||
setActiveQuickTool(null);
|
||||
setComposerMenu(null);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const goSmartCutoutPrevious = () => {
|
||||
if (!smartCutoutImage) {
|
||||
closeSmartCutoutTool();
|
||||
return;
|
||||
}
|
||||
runSmartCutoutPageTransition(
|
||||
{
|
||||
title: "正在返回上一页",
|
||||
subtitle: "回到图片上传页",
|
||||
},
|
||||
() => {
|
||||
setSmartCutoutBatchImages((current) => {
|
||||
revokeSmartCutoutItems(current);
|
||||
return [];
|
||||
});
|
||||
setSmartCutoutImage((current) => {
|
||||
if (current?.src) URL.revokeObjectURL(current.src);
|
||||
return null;
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const addSmartCutoutImage = (files: File[]) => {
|
||||
const imageFiles = files.filter((file) => file.type.startsWith("image/"));
|
||||
if (!imageFiles.length) {
|
||||
toast.error("请上传图片文件");
|
||||
return;
|
||||
}
|
||||
clearSmartCutoutTransition();
|
||||
setSmartCutoutBatchImages((current) => {
|
||||
revokeSmartCutoutItems(current);
|
||||
return [];
|
||||
});
|
||||
setSmartCutoutImage((current) => {
|
||||
if (current?.src) URL.revokeObjectURL(current.src);
|
||||
return null;
|
||||
});
|
||||
const nextImages = imageFiles.map((file) => ({ src: URL.createObjectURL(file), name: file.name }));
|
||||
smartCutoutPendingUrlsRef.current = nextImages.map((item) => item.src);
|
||||
setActiveQuickTool("cutout");
|
||||
setSmartCutoutSizeKey("original");
|
||||
setSmartCutoutTransitionMessage({
|
||||
title: imageFiles.length > 1 ? "正在批量抠图" : "正在智能抠图",
|
||||
subtitle: imageFiles.length > 1 ? `正在处理 ${imageFiles.length} 张图片` : "即将进入图片编辑室",
|
||||
});
|
||||
setIsSmartCutoutTransitioning(true);
|
||||
smartCutoutTransitionTimeoutRef.current = window.setTimeout(() => {
|
||||
smartCutoutTransitionTimeoutRef.current = null;
|
||||
smartCutoutPendingUrlsRef.current = [];
|
||||
setSmartCutoutBatchImages(nextImages);
|
||||
setSmartCutoutImage(nextImages[0]);
|
||||
setIsSmartCutoutTransitioning(false);
|
||||
}, 620);
|
||||
};
|
||||
|
||||
const handleSmartCutoutUpload = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (!files?.length) return;
|
||||
addSmartCutoutImage(Array.from(files));
|
||||
event.target.value = "";
|
||||
};
|
||||
|
||||
const handleSmartCutoutDrop = (event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setIsSmartCutoutDragging(false);
|
||||
const files = Array.from(event.dataTransfer.files);
|
||||
if (files.length) addSmartCutoutImage(files);
|
||||
};
|
||||
|
||||
const smartCutoutBackgroundValue = useMemo(() => {
|
||||
const rgb = hexToRgb(smartCutoutBackgroundColor) ?? { r: 255, g: 255, b: 255 };
|
||||
if (smartCutoutBackgroundAlpha >= 100) return smartCutoutBackgroundColor;
|
||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${Math.round(smartCutoutBackgroundAlpha) / 100})`;
|
||||
}, [smartCutoutBackgroundAlpha, smartCutoutBackgroundColor]);
|
||||
|
||||
const smartCutoutColorHsv = useMemo(() => hexToHsv(smartCutoutBackgroundColor), [smartCutoutBackgroundColor]);
|
||||
|
||||
const selectedSmartCutoutSize = useMemo(
|
||||
() => smartCutoutSizeOptions.find((option) => option.key === smartCutoutSizeKey) ?? smartCutoutSizeOptions[0],
|
||||
[smartCutoutSizeKey],
|
||||
);
|
||||
|
||||
const smartCutoutFrameStyle = useMemo<CSSProperties>(
|
||||
() => ({
|
||||
"--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,
|
||||
} as CSSProperties),
|
||||
[selectedSmartCutoutSize, smartCutoutBackgroundValue],
|
||||
);
|
||||
|
||||
const applySmartCutoutHsv = (h: number, s: number, v: number) => {
|
||||
const rgb = hsvToRgb(h, s, v);
|
||||
setSmartCutoutBackgroundColor(rgbToHex(rgb.r, rgb.g, rgb.b));
|
||||
};
|
||||
|
||||
const updateSmartCutoutColorFromPoint = (element: HTMLElement, clientX: number, clientY: number) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const saturation = clampNumber(((clientX - rect.left) / rect.width) * 100, 0, 100);
|
||||
const value = clampNumber(100 - ((clientY - rect.top) / rect.height) * 100, 0, 100);
|
||||
applySmartCutoutHsv(smartCutoutColorHsv.h, saturation, value);
|
||||
};
|
||||
|
||||
const handleSmartCutoutColorPlanePointer = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
updateSmartCutoutColorFromPoint(event.currentTarget, event.clientX, event.clientY);
|
||||
};
|
||||
|
||||
const handleSmartCutoutColorPlaneMove = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||
if (event.buttons !== 1) return;
|
||||
updateSmartCutoutColorFromPoint(event.currentTarget, event.clientX, event.clientY);
|
||||
};
|
||||
|
||||
const handleSmartCutoutHexChange = (value: string) => {
|
||||
const nextValue = value.startsWith("#") ? value : `#${value}`;
|
||||
if (!/^#[0-9a-fA-F]{0,6}$/.test(nextValue)) return;
|
||||
setSmartCutoutHexDraft(nextValue);
|
||||
const normalized = normalizeHexColor(nextValue);
|
||||
if (normalized) setSmartCutoutBackgroundColor(normalized);
|
||||
};
|
||||
|
||||
const scrollSmartCutoutTools = (direction: -1 | 1) => {
|
||||
smartCutoutToolsRef.current?.scrollBy({
|
||||
left: direction * 340,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSmartCutoutHexDraft(smartCutoutBackgroundColor);
|
||||
}, [smartCutoutBackgroundColor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSmartCutoutPaletteOpen) return undefined;
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
if (!smartCutoutPaletteRef.current?.contains(event.target as Node)) {
|
||||
setIsSmartCutoutPaletteOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("pointerdown", handlePointerDown);
|
||||
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
||||
}, [isSmartCutoutPaletteOpen]);
|
||||
|
||||
const removeSetImage = (imageId: string) => {
|
||||
setSetImages((current) => {
|
||||
const next = current.filter((item) => item.id !== imageId);
|
||||
@@ -1869,6 +2237,34 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
||||
}, [composerMenu, isCommandComposerCompact, status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (composerMenuCloseTimeoutRef.current !== null) {
|
||||
window.clearTimeout(composerMenuCloseTimeoutRef.current);
|
||||
composerMenuCloseTimeoutRef.current = null;
|
||||
}
|
||||
if (composerMenu) {
|
||||
setVisibleComposerMenu(composerMenu);
|
||||
setIsComposerMenuClosing(false);
|
||||
return;
|
||||
}
|
||||
if (!visibleComposerMenu) return;
|
||||
setIsComposerMenuClosing(true);
|
||||
composerMenuCloseTimeoutRef.current = window.setTimeout(() => {
|
||||
composerMenuCloseTimeoutRef.current = null;
|
||||
setVisibleComposerMenu(null);
|
||||
setIsComposerMenuClosing(false);
|
||||
}, 220);
|
||||
}, [composerMenu, visibleComposerMenu]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (composerMenuCloseTimeoutRef.current !== null) {
|
||||
window.clearTimeout(composerMenuCloseTimeoutRef.current);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!openCloneModelSelect) return undefined;
|
||||
|
||||
@@ -2462,6 +2858,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const isDetail = activeTool === "detail";
|
||||
const isTryOn = activeTool === "wear";
|
||||
const isCloneTool = activeTool === "clone";
|
||||
const isSmartCutoutTool = isCloneTool && activeQuickTool === "cutout";
|
||||
const pageLabel = isSetTool ? "鍟嗗搧濂楀浘" : isDetail ? "A+/璇︽儏椤?" : isTryOn ? "AI鏈嶉グ绌挎埓" : activeToolMeta?.label || "鍟嗗搧宸ュ叿";
|
||||
const setPrimaryLabel =
|
||||
setImages.length === 0
|
||||
@@ -2948,10 +3345,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
countries: marketLanguageOptions.filter((option) => option.languages.includes(item)).map((option) => option.country),
|
||||
}));
|
||||
const composerPopoverStyle: CSSProperties = { left: composerPopoverLeft };
|
||||
if (!composerMenu) return null;
|
||||
if (composerMenu === "mode") {
|
||||
const menuToRender = composerMenu ?? visibleComposerMenu;
|
||||
if (!menuToRender) return null;
|
||||
const popoverClosingClass = !composerMenu && isComposerMenuClosing ? " is-closing" : "";
|
||||
const composerPopoverKey = `${menuToRender}-${cloneOutput}-${popoverClosingClass ? "closing" : "open"}`;
|
||||
if (menuToRender === "mode") {
|
||||
return (
|
||||
<div className="ecom-command-popover ecom-command-popover--grid" style={composerPopoverStyle}>
|
||||
<div key={composerPopoverKey} className={`ecom-command-popover ecom-command-popover--grid${popoverClosingClass}`} style={composerPopoverStyle}>
|
||||
{visibleCloneOutputOptions.map((option) => (
|
||||
<button key={option.key} type="button" className={cloneOutput === option.key ? "is-active" : ""} onClick={() => { handleCloneOutputChange(option.key); setComposerMenu(null); }}>
|
||||
{option.label}
|
||||
@@ -2960,9 +3360,9 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (composerMenu === "platform") {
|
||||
if (menuToRender === "platform") {
|
||||
return (
|
||||
<div className="ecom-command-popover ecom-command-popover--list ecom-command-popover--ratio ecom-command-popover--platform" style={composerPopoverStyle}>
|
||||
<div key={composerPopoverKey} className={`ecom-command-popover ecom-command-popover--list ecom-command-popover--ratio ecom-command-popover--platform${popoverClosingClass}`} style={composerPopoverStyle}>
|
||||
{platformOptions.map((option) => (
|
||||
<button key={option} type="button" className={platform === option ? "is-active" : ""} onClick={() => { handleClonePlatformChange(option); setComposerMenu(null); }}>
|
||||
{renderPlatformLogo(option)}
|
||||
@@ -2972,9 +3372,9 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (composerMenu === "language") {
|
||||
if (menuToRender === "language") {
|
||||
return (
|
||||
<div className="ecom-command-popover ecom-command-popover--languages" style={composerPopoverStyle}>
|
||||
<div key={composerPopoverKey} className={`ecom-command-popover ecom-command-popover--languages${popoverClosingClass}`} style={composerPopoverStyle}>
|
||||
{composerLanguageOptions.map((option) => (
|
||||
<button key={option.language} type="button" className={language === option.language ? "is-active" : ""} onClick={() => { setLanguage(option.language); setComposerMenu(null); }}>
|
||||
<strong>{option.language}</strong>
|
||||
@@ -2984,9 +3384,9 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (composerMenu === "ratio") {
|
||||
if (menuToRender === "ratio") {
|
||||
return (
|
||||
<div className="ecom-command-popover ecom-command-popover--list" style={composerPopoverStyle}>
|
||||
<div key={composerPopoverKey} className={`ecom-command-popover ecom-command-popover--list${popoverClosingClass}`} style={composerPopoverStyle}>
|
||||
{cloneRatioOptions.map((option) => (
|
||||
<button key={option} type="button" className={ratio === option ? "is-active" : ""} onClick={() => { setRatio(option); setComposerMenu(null); }}>
|
||||
<span className="ecom-command-button-text">{formatRatioDisplayValue(option)}</span>
|
||||
@@ -2996,7 +3396,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={`ecom-command-popover ecom-command-popover--settings ecom-command-popover--settings-${cloneOutput}`} style={composerPopoverStyle}>
|
||||
<div key={composerPopoverKey} className={`ecom-command-popover ecom-command-popover--settings ecom-command-popover--settings-${cloneOutput}${popoverClosingClass}`} style={composerPopoverStyle}>
|
||||
{cloneOutput === "set" ? (
|
||||
<>
|
||||
<header><strong>套图分类设置</strong><span>总数 1-16 张</span></header>
|
||||
@@ -3299,7 +3699,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : (
|
||||
) : status === "idle" || status === "ready" ? null : (
|
||||
<section className="clone-ai-empty-state" aria-live="polite">
|
||||
{status === "generating" ? <LoadingOutlined /> : status === "failed" ? <FrownOutlined /> : <FileImageOutlined />}
|
||||
<strong>{status === "generating" ? "正在生成" : status === "failed" ? "生成失败" : "等待生成"}</strong>
|
||||
@@ -3346,6 +3746,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
className="ecom-command-hidden-file"
|
||||
onChange={handleComposerAssetUpload}
|
||||
/>
|
||||
<input
|
||||
ref={smartCutoutInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="ecom-command-hidden-file"
|
||||
onChange={handleSmartCutoutUpload}
|
||||
/>
|
||||
<div className="clone-ai-input-wrapper ecom-command-composer">
|
||||
<button
|
||||
type="button"
|
||||
@@ -3432,18 +3840,23 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
<strong>添加</strong>
|
||||
</button>
|
||||
<button type="button" className={composerMenu === "mode" ? "is-active" : ""} onClick={(event) => toggleComposerMenu("mode", event)}>
|
||||
<span className="ecom-command-option-icon" aria-hidden="true"><AppstoreOutlined /></span>
|
||||
{selectedCloneOutput.label}<span>模式</span>
|
||||
</button>
|
||||
<button type="button" className={composerMenu === "platform" ? "is-active" : ""} onClick={(event) => toggleComposerMenu("platform", event)}>
|
||||
<span className="ecom-command-option-icon" aria-hidden="true"><GlobalOutlined /></span>
|
||||
<span>平台</span>{platform}
|
||||
</button>
|
||||
<button type="button" className={composerMenu === "language" ? "is-active" : ""} onClick={(event) => toggleComposerMenu("language", event)}>
|
||||
<span className="ecom-command-option-icon" aria-hidden="true"><FileImageOutlined /></span>
|
||||
<span>语种</span>{language}
|
||||
</button>
|
||||
<button type="button" className={composerMenu === "ratio" ? "is-active" : ""} onClick={(event) => toggleComposerMenu("ratio", event)}>
|
||||
<span className="ecom-command-option-icon" aria-hidden="true"><TableOutlined /></span>
|
||||
<span>比例</span>{formatRatioDisplayValue(ratio)}
|
||||
</button>
|
||||
<button type="button" className={composerMenu === "settings" ? "is-active" : ""} onClick={(event) => toggleComposerMenu("settings", event)}>
|
||||
<span className="ecom-command-option-icon" aria-hidden="true"><SettingOutlined /></span>
|
||||
<span>设置</span>{composerSettingLabel}
|
||||
</button>
|
||||
</div>
|
||||
@@ -3455,11 +3868,292 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</div>
|
||||
{renderComposerMenu()}
|
||||
</div>
|
||||
{status !== "done" ? (
|
||||
<section className="ecom-command-quick-board" aria-label="快捷功能">
|
||||
{[
|
||||
{ label: "智能抠图", icon: <FileImageOutlined />, onClick: openSmartCutoutUpload },
|
||||
{ label: "商品套图", icon: <AppstoreOutlined /> },
|
||||
{ label: "图片修改", icon: <SettingOutlined /> },
|
||||
{ label: "增/去水印", icon: <FileImageOutlined /> },
|
||||
{ label: "图片批处理", icon: <FolderOpenOutlined /> },
|
||||
{ label: "一键翻译", icon: <FileImageOutlined /> },
|
||||
{ label: "A+/详情页", icon: <FileImageOutlined /> },
|
||||
{ label: "变清晰", icon: <FileImageOutlined /> },
|
||||
{ label: "AI消除", icon: <SettingOutlined /> },
|
||||
{ label: "证件照", icon: <SkinOutlined /> },
|
||||
{ label: "爆款视频", icon: <CloudUploadOutlined /> },
|
||||
{ label: "拼图", icon: <TableOutlined /> },
|
||||
].map((item) => (
|
||||
<button key={item.label} type="button" onClick={item.onClick}>
|
||||
<span aria-hidden="true">{item.icon}</span>
|
||||
<strong>{item.label}</strong>
|
||||
</button>
|
||||
))}
|
||||
</section>
|
||||
) : null}
|
||||
<span className="clone-ai-char-count">{requirement.length}/500</span>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
|
||||
const smartCutoutPreview = (
|
||||
<main className={`ecom-smart-cutout-page${smartCutoutImage ? " is-editor" : " is-upload"}${isSmartCutoutTransitioning ? " is-transitioning" : ""}`} aria-label="智能抠图">
|
||||
<input
|
||||
ref={smartCutoutInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="ecom-command-hidden-file"
|
||||
onChange={handleSmartCutoutUpload}
|
||||
/>
|
||||
<nav className="ecom-smart-cutout-nav" aria-label="智能抠图导航">
|
||||
<button type="button" onClick={closeSmartCutoutTool}>
|
||||
首页
|
||||
</button>
|
||||
<button type="button" onClick={goSmartCutoutPrevious}>
|
||||
上一页
|
||||
</button>
|
||||
</nav>
|
||||
{isSmartCutoutTransitioning ? (
|
||||
<div className="ecom-smart-cutout-transition" role="status" aria-live="polite">
|
||||
<span aria-hidden="true" />
|
||||
<strong>{smartCutoutTransitionMessage.title}</strong>
|
||||
<em>{smartCutoutTransitionMessage.subtitle}</em>
|
||||
</div>
|
||||
) : null}
|
||||
{!smartCutoutImage ? (
|
||||
<section className="ecom-smart-cutout-upload">
|
||||
<div className="ecom-smart-cutout-head">
|
||||
<strong>智能抠图</strong>
|
||||
<span>3s 一键抠图,快速去除背景</span>
|
||||
</div>
|
||||
<div className="ecom-smart-cutout-upload__body">
|
||||
<div className="ecom-smart-cutout-demo" aria-hidden="true">
|
||||
<div className="ecom-smart-cutout-demo__tile ecom-smart-cutout-demo__tile--flower" />
|
||||
<div className="ecom-smart-cutout-demo__tile ecom-smart-cutout-demo__tile--product" />
|
||||
<div className="ecom-smart-cutout-demo__tile ecom-smart-cutout-demo__tile--poster" />
|
||||
<div className="ecom-smart-cutout-demo__tile ecom-smart-cutout-demo__tile--object" />
|
||||
</div>
|
||||
<div
|
||||
className={`ecom-smart-cutout-upload-box${isSmartCutoutDragging ? " is-dragging" : ""}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => smartCutoutInputRef.current?.click()}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
smartCutoutInputRef.current?.click();
|
||||
}
|
||||
}}
|
||||
onDragEnter={(event) => {
|
||||
event.preventDefault();
|
||||
setIsSmartCutoutDragging(true);
|
||||
}}
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
onDragLeave={() => setIsSmartCutoutDragging(false)}
|
||||
onDrop={handleSmartCutoutDrop}
|
||||
>
|
||||
<button type="button" className="ecom-smart-cutout-upload__primary">
|
||||
<CloudUploadOutlined />
|
||||
上传图片
|
||||
</button>
|
||||
<button type="button" className="ecom-smart-cutout-upload__secondary">
|
||||
<FolderOpenOutlined />
|
||||
批量上传
|
||||
</button>
|
||||
<span>上传文件夹 ></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<section className="ecom-smart-editor">
|
||||
<div className="ecom-smart-editor__workspace">
|
||||
<div className="ecom-smart-editor__canvas">
|
||||
<div className={`ecom-smart-editor__checker is-size-${smartCutoutSizeKey}`} style={smartCutoutFrameStyle}>
|
||||
<div className="ecom-smart-editor__background-layer" aria-hidden="true" />
|
||||
<img src={smartCutoutImage.src} alt={smartCutoutImage.name} />
|
||||
</div>
|
||||
<div className="ecom-smart-editor__canvas-actions">
|
||||
<button type="button">对比</button>
|
||||
<button type="button">编辑图片</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ecom-smart-editor__tools-shell">
|
||||
<button type="button" className="ecom-smart-editor__tools-nav" onClick={() => scrollSmartCutoutTools(-1)} aria-label="查看上一组尺寸">
|
||||
‹
|
||||
</button>
|
||||
<div className="ecom-smart-editor__tools" ref={smartCutoutToolsRef}>
|
||||
{smartCutoutSizeOptions.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={smartCutoutSizeKey === item.key ? "is-active" : ""}
|
||||
onClick={() => setSmartCutoutSizeKey(item.key)}
|
||||
aria-pressed={smartCutoutSizeKey === item.key}
|
||||
>
|
||||
<span className={`ecom-smart-editor__tool-icon ecom-smart-editor__tool-icon--${item.icon}`} aria-hidden="true" />
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button type="button" className="ecom-smart-editor__tools-nav" onClick={() => scrollSmartCutoutTools(1)} aria-label="查看更多尺寸">
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
{smartCutoutBatchImages.length > 1 ? (
|
||||
<section className="ecom-smart-editor__batch" aria-label="批量图片">
|
||||
<header>
|
||||
<strong>批量图片</strong>
|
||||
<span>{smartCutoutBatchImages.length} 张</span>
|
||||
</header>
|
||||
<div>
|
||||
{smartCutoutBatchImages.map((image, index) => (
|
||||
<button
|
||||
key={`${image.src}-${index}`}
|
||||
type="button"
|
||||
className={smartCutoutImage.src === image.src ? "is-active" : ""}
|
||||
onClick={() => setSmartCutoutImage(image)}
|
||||
>
|
||||
<img src={image.src} alt={image.name || `上传图片 ${index + 1}`} />
|
||||
<span>{index + 1}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
<section className="ecom-smart-editor__gallery">
|
||||
<header><strong>纯色</strong><button type="button">查看全部 ></button></header>
|
||||
<div className="ecom-smart-editor__swatches">
|
||||
{["#ffffff", "#eeeae3", "#f2e3cf", "#000000", "#a89682", "#c9c9c9"].map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
className={smartCutoutBackgroundColor.toLowerCase() === color ? "is-active" : ""}
|
||||
style={{ "--smart-cutout-swatch-bg": color } as CSSProperties}
|
||||
onClick={() => {
|
||||
setSmartCutoutBackgroundColor(color);
|
||||
setSmartCutoutBackgroundAlpha(100);
|
||||
}}
|
||||
>
|
||||
<span className="ecom-smart-editor__swatch-bg" aria-hidden="true" />
|
||||
<img src={smartCutoutImage.src} alt="" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<header><strong>AI换背景</strong><button type="button">查看全部 ></button></header>
|
||||
<div className="ecom-smart-editor__scenes">
|
||||
<button type="button" className="ecom-smart-editor__generate"><SettingOutlined />立即生成</button>
|
||||
{["客厅陈列", "桌面日光", "香氛产品", "绿植窗边", "居家空间"].map((item) => (
|
||||
<button key={item} type="button"><span>{item}</span></button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<aside className="ecom-smart-editor__side">
|
||||
<strong>背景颜色</strong>
|
||||
<div className="ecom-smart-editor__color-row">
|
||||
<div className="ecom-smart-editor__color-wrap" ref={smartCutoutPaletteRef} style={{ "--smart-cutout-bg": smartCutoutBackgroundValue } as CSSProperties}>
|
||||
<button
|
||||
type="button"
|
||||
className={`ecom-smart-editor__custom-color${isSmartCutoutPaletteOpen ? " is-active" : ""}`}
|
||||
style={{ background: smartCutoutBackgroundColor }}
|
||||
onClick={() => setIsSmartCutoutPaletteOpen((open) => !open)}
|
||||
aria-label="打开背景调色盘"
|
||||
>
|
||||
<span>色</span>
|
||||
</button>
|
||||
{isSmartCutoutPaletteOpen ? (
|
||||
<div className="ecom-smart-color-picker" role="dialog" aria-label="背景调色盘">
|
||||
<button
|
||||
type="button"
|
||||
className="ecom-smart-color-picker__plane"
|
||||
style={{ background: `linear-gradient(to top, #000000, transparent), linear-gradient(to right, #ffffff, hsl(${smartCutoutColorHsv.h} 100% 50%))` }}
|
||||
onPointerDown={handleSmartCutoutColorPlanePointer}
|
||||
onPointerMove={handleSmartCutoutColorPlaneMove}
|
||||
aria-label="选择颜色明度和饱和度"
|
||||
>
|
||||
<span style={{ left: `${smartCutoutColorHsv.s}%`, top: `${100 - smartCutoutColorHsv.v}%` }} />
|
||||
</button>
|
||||
<div className="ecom-smart-color-picker__slider ecom-smart-color-picker__slider--hue">
|
||||
<span style={{ background: `hsl(${smartCutoutColorHsv.h} 100% 50%)` }} />
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={360}
|
||||
value={smartCutoutColorHsv.h}
|
||||
onChange={(event) => applySmartCutoutHsv(Number(event.target.value), smartCutoutColorHsv.s, smartCutoutColorHsv.v)}
|
||||
aria-label="色相"
|
||||
/>
|
||||
</div>
|
||||
<div className="ecom-smart-color-picker__slider ecom-smart-color-picker__slider--alpha">
|
||||
<span style={{ background: smartCutoutBackgroundValue }} />
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
value={smartCutoutBackgroundAlpha}
|
||||
onChange={(event) => setSmartCutoutBackgroundAlpha(Number(event.target.value))}
|
||||
aria-label="透明度"
|
||||
/>
|
||||
</div>
|
||||
<div className="ecom-smart-color-picker__fields">
|
||||
<span aria-hidden="true">↙</span>
|
||||
<input
|
||||
value={smartCutoutHexDraft}
|
||||
onChange={(event) => handleSmartCutoutHexChange(event.target.value)}
|
||||
onBlur={() => setSmartCutoutHexDraft(smartCutoutBackgroundColor)}
|
||||
aria-label="HEX 色值"
|
||||
/>
|
||||
<input
|
||||
value={Math.round(smartCutoutBackgroundAlpha)}
|
||||
onChange={(event) => setSmartCutoutBackgroundAlpha(clampNumber(Number(event.target.value) || 0, 0, 100))}
|
||||
aria-label="透明度百分比"
|
||||
/>
|
||||
<strong>%</strong>
|
||||
</div>
|
||||
<p>纯色预设</p>
|
||||
<div className="ecom-smart-color-picker__presets">
|
||||
{smartCutoutColorPresets.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
className={smartCutoutBackgroundColor.toLowerCase() === color ? "is-active" : ""}
|
||||
style={{ background: color }}
|
||||
onClick={() => {
|
||||
setSmartCutoutBackgroundColor(color);
|
||||
setSmartCutoutBackgroundAlpha(100);
|
||||
}}
|
||||
aria-label={`选择颜色 ${color}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{["#ffffff", "#f8f9fa", "#000000", "#bdbdbd"].map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
className={smartCutoutBackgroundColor.toLowerCase() === color ? "is-active" : ""}
|
||||
style={{ background: color }}
|
||||
onClick={() => {
|
||||
setSmartCutoutBackgroundColor(color);
|
||||
setSmartCutoutBackgroundAlpha(100);
|
||||
}}
|
||||
aria-label={`背景颜色 ${color}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="ecom-smart-editor__side-actions">
|
||||
<button type="button" className="ecom-smart-editor__download">下载图片</button>
|
||||
<button type="button" onClick={() => smartCutoutInputRef.current?.click()}>再次上传</button>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
|
||||
const detailPreview = (
|
||||
<main className="product-clone-preview product-clone-preview--detail" aria-label="A+详情预览" onWheel={handlePreviewWheel}>
|
||||
<div className="product-clone-preview__headline">
|
||||
@@ -3557,7 +4251,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`product-clone-page page-motion${isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : ""}${isCloneTool && isCommandHistoryCollapsed ? " is-history-collapsed" : ""}`}
|
||||
className={`product-clone-page page-motion${isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : ""}${isCloneTool && isCommandHistoryCollapsed ? " is-history-collapsed" : ""}${isSmartCutoutTool ? " is-smart-cutout-page" : ""}`}
|
||||
data-tool={activeTool}
|
||||
aria-label={pageLabel}
|
||||
>
|
||||
@@ -3581,7 +4275,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
{isSetTool ? setPanel : isDetail ? detailPanel : isTryOn ? tryOnPanel : isCloneTool ? clonePanel : placeholderPanel}
|
||||
</aside>
|
||||
|
||||
{isCloneTool ? (
|
||||
{isCloneTool && !isSmartCutoutTool ? (
|
||||
<button
|
||||
type="button"
|
||||
className="clone-ai-settings-toggle"
|
||||
@@ -3595,7 +4289,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{isSetTool ? setPreview : isDetail ? detailPreview : isTryOn ? tryOnPreview : isCloneTool ? (false ? (
|
||||
{isSetTool ? setPreview : isDetail ? detailPreview : isTryOn ? tryOnPreview : isCloneTool ? (isSmartCutoutTool ? smartCutoutPreview : false ? (
|
||||
<main className="product-clone-preview product-clone-preview--video" style={{ padding: 0 }}>
|
||||
<EcommerceVideoWorkspace
|
||||
isAuthenticated={isAuthenticated}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user