Merge branch 'main' into feat/ecommerce-record-detail-conversation-panel
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import {
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
ClearOutlined,
|
||||
CloudUploadOutlined,
|
||||
@@ -16,6 +16,7 @@
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
PaperClipOutlined,
|
||||
PlusOutlined,
|
||||
QuestionCircleOutlined,
|
||||
ReloadOutlined,
|
||||
ScissorOutlined,
|
||||
@@ -281,6 +282,12 @@ type CloneBasicSelectKey = "platform" | "market" | "language" | "ratio";
|
||||
type CloneModelSelectKey = "gender" | "age" | "ethnicity" | "body";
|
||||
type CloneReferenceMode = "upload" | "link";
|
||||
type CloneReplicateLevelKey = "style" | "high";
|
||||
type CloneTemplateAsset = {
|
||||
id: string;
|
||||
title: string;
|
||||
prompt: string;
|
||||
mediaUrl: string;
|
||||
};
|
||||
type TryOnModelSource = "ai" | "library";
|
||||
type TryOnStatus = "idle" | "modeling" | "ready" | "generating" | "done" | "failed";
|
||||
type DetailStatus = "idle" | "ready" | "generating" | "done" | "failed";
|
||||
@@ -1027,10 +1034,115 @@ const productSetOutputOptions: Array<{ key: ProductSetOutputKey; label: string;
|
||||
{ key: "model", label: "模特图", desc: "真人穿搭展示", icon: <SkinOutlined /> },
|
||||
{ key: "video", label: "短视频", desc: "分镜视频链路", icon: <VideoCameraOutlined /> },
|
||||
];
|
||||
const cloneOutputOptions: Array<{ key: CloneOutputKey; label: string; desc: string; icon: ReactNode }> = [
|
||||
const cloneOutputOptions: Array<{ key: ProductSetOutputKey; label: string; desc: string; icon: ReactNode }> = [
|
||||
...productSetOutputOptions,
|
||||
{ key: "hot", label: "爆款复刻", desc: "参考图风格迁移", icon: <FireOutlined /> },
|
||||
];
|
||||
const cloneTemplateCards: Record<Exclude<CloneOutputKey, "hot">, CloneTemplateAsset[]> = {
|
||||
set: [
|
||||
{
|
||||
id: "set-main",
|
||||
title: "商品套图主图",
|
||||
prompt: "生成一组统一风格的商品套图,包含主图、卖点图、场景图和细节图,主体清晰,色调统一,符合电商平台展示规范。",
|
||||
mediaUrl: ossAssets.ecommerce.productSet.main,
|
||||
},
|
||||
{
|
||||
id: "set-scene",
|
||||
title: "商品套图场景",
|
||||
prompt: "生成生活化场景商品套图,突出商品在真实环境中的使用感、氛围感和转化卖点。",
|
||||
mediaUrl: ossAssets.ecommerce.productSet.scene,
|
||||
},
|
||||
{
|
||||
id: "set-detail",
|
||||
title: "商品套图细节",
|
||||
prompt: "生成突出材质、工艺和边缘细节的商品套图,画面干净,信息聚焦,适合电商详情展示。",
|
||||
mediaUrl: ossAssets.ecommerce.productSet.detail,
|
||||
},
|
||||
{
|
||||
id: "set-selling",
|
||||
title: "商品套图卖点",
|
||||
prompt: "生成强调核心卖点和对比优势的商品套图,信息层级清晰,适合列表页和转化场景。",
|
||||
mediaUrl: ossAssets.ecommerce.productSet.selling,
|
||||
},
|
||||
],
|
||||
detail: [
|
||||
{
|
||||
id: "detail-hero",
|
||||
title: "详情图头图",
|
||||
prompt: "生成适用于 A+ 详情页的头图模块,突出品牌感、主卖点和视觉中心,版式清晰高级。",
|
||||
mediaUrl: ossAssets.ecommerce.detail.longPage,
|
||||
},
|
||||
{
|
||||
id: "detail-grid-a",
|
||||
title: "详情图模块 A",
|
||||
prompt: "生成模块化详情长图,重点展示产品卖点、功能说明和适用场景,适合滚动阅读。",
|
||||
mediaUrl: ossAssets.ecommerce.detail.gridA,
|
||||
},
|
||||
{
|
||||
id: "detail-grid-b",
|
||||
title: "详情图模块 B",
|
||||
prompt: "生成模块化详情长图,强化材质、规格和使用说明,视觉简洁,信息明确。",
|
||||
mediaUrl: ossAssets.ecommerce.detail.gridB,
|
||||
},
|
||||
{
|
||||
id: "detail-grid-c",
|
||||
title: "详情图模块 C",
|
||||
prompt: "生成模块化详情页内容,突出品牌叙事、细节拆解和购买理由,保持统一排版。",
|
||||
mediaUrl: ossAssets.ecommerce.detail.gridC,
|
||||
},
|
||||
],
|
||||
model: [
|
||||
{
|
||||
id: "model-dress-a",
|
||||
title: "模特图穿搭 A",
|
||||
prompt: "生成真人模特穿搭展示图,突出服装版型、上身效果和整体气质,姿态自然。",
|
||||
mediaUrl: ossAssets.ecommerce.tryOn.dressA,
|
||||
},
|
||||
{
|
||||
id: "model-dress-b",
|
||||
title: "模特图穿搭 B",
|
||||
prompt: "生成适合商品展示的模特图,强调衣型、垂感和真实穿着效果,画面干净。",
|
||||
mediaUrl: ossAssets.ecommerce.tryOn.dressB,
|
||||
},
|
||||
{
|
||||
id: "model-woman",
|
||||
title: "模特图女模",
|
||||
prompt: "生成自然站姿的女模特展示图,适合服饰、配件和穿搭类商品展示。",
|
||||
mediaUrl: ossAssets.ecommerce.tryOn.modelWoman,
|
||||
},
|
||||
{
|
||||
id: "model-man",
|
||||
title: "模特图男模",
|
||||
prompt: "生成真实感更强的男模特展示图,突出上身效果、轮廓和场景氛围。",
|
||||
mediaUrl: ossAssets.ecommerce.tryOn.modelMan,
|
||||
},
|
||||
],
|
||||
video: [
|
||||
{
|
||||
id: "video-hook",
|
||||
title: "短视频开场",
|
||||
prompt: "生成适合电商短视频的开场镜头,节奏明确,第一秒就突出产品和核心看点。",
|
||||
mediaUrl: ossAssets.ecommerce.inspiration.tiktokPreference,
|
||||
},
|
||||
{
|
||||
id: "video-scene",
|
||||
title: "短视频场景",
|
||||
prompt: "生成生活化使用场景的短视频分镜,画面连贯,围绕商品使用过程展开。",
|
||||
mediaUrl: ossAssets.ecommerce.inspiration.officeStyleSet,
|
||||
},
|
||||
{
|
||||
id: "video-review",
|
||||
title: "短视频口播",
|
||||
prompt: "生成适合口播讲解的电商短视频结构,包含产品亮点、卖点说明和收尾引导。",
|
||||
mediaUrl: ossAssets.ecommerce.inspiration.asinListing,
|
||||
},
|
||||
{
|
||||
id: "video-conversion",
|
||||
title: "短视频转化",
|
||||
prompt: "生成以转化为目标的短视频分镜,强化开头钩子、卖点展示和行动引导。",
|
||||
mediaUrl: ossAssets.ecommerce.inspiration.competitorListing,
|
||||
},
|
||||
],
|
||||
};
|
||||
const cloneSetCountOptions: Array<{
|
||||
key: CloneSetCountKey;
|
||||
title: string;
|
||||
@@ -1477,6 +1589,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const garmentInputRef = useRef<HTMLInputElement>(null);
|
||||
const detailInputRef = useRef<HTMLInputElement>(null);
|
||||
const detailProgressRef = useRef<number | null>(null);
|
||||
const hotProgressRef = useRef<number | null>(null);
|
||||
const hotMaterialInputRef = useRef<HTMLInputElement>(null);
|
||||
const countHoldTimeoutRef = useRef<number | null>(null);
|
||||
const countHoldIntervalRef = useRef<number | null>(null);
|
||||
const isAuthenticated = Boolean((_props as Record<string, unknown>).isAuthenticated);
|
||||
@@ -1510,7 +1624,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const [selectedProductSetPreview, setSelectedProductSetPreview] = useState<ProductSetPreviewSelection | null>(null);
|
||||
const [showHostingModal, setShowHostingModal] = useState(false);
|
||||
const [productImages, setProductImages] = useState<CloneImageItem[]>([]);
|
||||
const [activeQuickTool, setActiveQuickTool] = useState<"cutout" | "detail" | "watermark" | "image-edit" | "translate" | null>(null);
|
||||
const [activeQuickTool, setActiveQuickTool] = useState<"cutout" | "detail" | "watermark" | "image-edit" | "translate" | "hot" | null>(null);
|
||||
const [smartCutoutImage, setSmartCutoutImage] = useState<SmartCutoutImageItem | null>(null);
|
||||
const [smartCutoutBatchImages, setSmartCutoutBatchImages] = useState<SmartCutoutImageItem[]>([]);
|
||||
const [smartCutoutBackgroundColor, setSmartCutoutBackgroundColor] = useState("#ffffff");
|
||||
@@ -1547,6 +1661,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const [imageWorkbenchProgress, setImageWorkbenchProgress] = useState(0);
|
||||
const [isProductUploadDragging, setIsProductUploadDragging] = useState(false);
|
||||
const [cloneOutput, setCloneOutput] = useState<CloneOutputKey>(defaultCloneOutput);
|
||||
const [isCloneTemplateStripVisible, setIsCloneTemplateStripVisible] = useState(false);
|
||||
const [videoHistoryVisible, setVideoHistoryVisible] = useState(false);
|
||||
const [isVideoWorkspaceVisible, setIsVideoWorkspaceVisible] = useState(false);
|
||||
const [videoPlanTrigger, setVideoPlanTrigger] = useState(0);
|
||||
@@ -1998,24 +2113,25 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const [detailStatus, setDetailStatus] = useState<DetailStatus>("idle");
|
||||
const [detailResultUrl, setDetailResultUrl] = useState<string | null>(null);
|
||||
const [detailProgress, setDetailProgress] = useState(0);
|
||||
const [hotRequirement, setHotRequirement] = useState("");
|
||||
const [isHotMaterialDragging, setIsHotMaterialDragging] = useState(false);
|
||||
const [hotMaterialHoverZoom, setHotMaterialHoverZoom] = useState<{ src: string; x: number; y: number; placement: "above" | "below" } | null>(null);
|
||||
const [hotPlatform, setHotPlatform] = useState(platformOptions[0]);
|
||||
const [hotMarket, setHotMarket] = useState(marketOptions[0]);
|
||||
const [hotLanguage, setHotLanguage] = useState(getPlatformDefaultLanguage(platformOptions[0], marketOptions[0]));
|
||||
const [hotRatio, setHotRatio] = useState(getQuickSetRatioValue(getPlatformDefaultRatio(platformOptions[0], "detail")));
|
||||
const [hotStatus, setHotStatus] = useState<DetailStatus>("idle");
|
||||
const [hotResultUrl, setHotResultUrl] = useState<string | null>(null);
|
||||
const [hotProgress, setHotProgress] = useState(0);
|
||||
const productSetRatioOptions = useMemo(
|
||||
() => getPlatformRatioOptions(productSetPlatform, productSetOutput),
|
||||
[productSetOutput, productSetPlatform],
|
||||
);
|
||||
const hotUploadedRatioOption = useMemo(
|
||||
() => cloneOutput === "hot" ? formatUploadedImageRatio(cloneReferenceImages[0]) : null,
|
||||
[cloneOutput, cloneReferenceImages],
|
||||
);
|
||||
const baseCloneRatioOptions = useMemo(
|
||||
() => getPlatformRatioOptions(platform, cloneOutput),
|
||||
[cloneOutput, platform],
|
||||
);
|
||||
const cloneRatioOptions = useMemo(
|
||||
() => hotUploadedRatioOption
|
||||
? getUniqueRatioOptions([...baseCloneRatioOptions, hotUploadedRatioOption])
|
||||
: baseCloneRatioOptions,
|
||||
[baseCloneRatioOptions, hotUploadedRatioOption],
|
||||
);
|
||||
const cloneRatioOptions = baseCloneRatioOptions;
|
||||
const productSetLanguageOptions = useMemo(
|
||||
() => getPlatformLanguageOptions(productSetPlatform, productSetMarket),
|
||||
[productSetMarket, productSetPlatform],
|
||||
@@ -2028,6 +2144,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
() => getPlatformLanguageOptions(detailPlatform, detailMarket),
|
||||
[detailMarket, detailPlatform],
|
||||
);
|
||||
const hotLanguageOptions = useMemo(
|
||||
() => getPlatformLanguageOptions(hotPlatform, hotMarket),
|
||||
[hotMarket, hotPlatform],
|
||||
);
|
||||
const ecommerceMentionImages: MentionImageOption[] = [
|
||||
...productImages.map((image, index) => ({ ...image, label: `商品图 ${index + 1}` })),
|
||||
...cloneReferenceImages.map((image, index) => ({ ...image, label: `参考图 ${index + 1}` })),
|
||||
@@ -2045,6 +2165,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const selectedProductSetOutput =
|
||||
productSetOutputOptions.find((option) => option.key === productSetOutput) ?? productSetOutputOptions[0]!;
|
||||
const selectedCloneOutput = cloneOutputOptions.find((option) => option.key === cloneOutput) ?? cloneOutputOptions[1]!;
|
||||
const activeCloneTemplateCards = cloneTemplateCards[cloneOutput === "hot" ? "set" : cloneOutput];
|
||||
const cloneRequirementPlaceholder =
|
||||
cloneOutput === "model"
|
||||
? "建议包含以下信息:产品名称、核心卖点、期望场景、模特外貌描述(如小麦色皮肤、齐刘海、眼角有泪痣)、具体参数"
|
||||
@@ -2058,6 +2179,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const canGenerate = productImages.length > 0 && status !== "generating";
|
||||
const canGenerateTryOn = garmentImages.length > 0 && tryOnStatus !== "generating" && tryOnStatus !== "modeling";
|
||||
const canGenerateDetail = detailProductImages.length > 0 && detailStatus !== "generating";
|
||||
const canGenerateHot = cloneReferenceImages.length > 0 && hotStatus !== "generating";
|
||||
const cloneVideoDurationProgress =
|
||||
((cloneVideoDuration - cloneVideoDurationMin) / (cloneVideoDurationMax - cloneVideoDurationMin)) * 100;
|
||||
const cloneVideoDurationStyle: CSSProperties = useMemo(
|
||||
@@ -2190,21 +2312,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
const openSmartCutoutUpload = () => {
|
||||
clearSmartCutoutTransition();
|
||||
setSmartCutoutTransitionMessage({
|
||||
title: "正在进入智能抠图",
|
||||
subtitle: "为你打开图片处理工具",
|
||||
});
|
||||
setActiveQuickTool("cutout");
|
||||
setSmartCutoutBatchImages((current) => {
|
||||
revokeSmartCutoutItems(current);
|
||||
return [];
|
||||
});
|
||||
setSmartCutoutImage((current) => {
|
||||
revokeSmartCutoutItem(current);
|
||||
return null;
|
||||
});
|
||||
setIsSmartCutoutComparing(false);
|
||||
setComposerMenu(null);
|
||||
toast.info("功能正在优化中");
|
||||
};
|
||||
|
||||
const openWatermarkRemovalPage = () => {
|
||||
@@ -2418,9 +2527,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
const openImageTranslatePage = () => {
|
||||
clearSmartCutoutTransition();
|
||||
setActiveQuickTool("translate");
|
||||
setComposerMenu(null);
|
||||
setIsCloneSettingsCollapsed(false);
|
||||
toast.info("功能正在优化中");
|
||||
};
|
||||
|
||||
const closeImageTranslatePage = () => {
|
||||
@@ -2954,6 +3062,16 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
if (!selectedDetailModules.length) setSelectedDetailModules(defaultCloneDetailModuleIds);
|
||||
};
|
||||
|
||||
const openHotClonePage = () => {
|
||||
clearSmartCutoutTransition();
|
||||
setActiveQuickTool("hot");
|
||||
setComposerMenu(null);
|
||||
setIsCloneSettingsCollapsed(false);
|
||||
setIsQuickPanelCollapsed(false);
|
||||
setPreviewZoom(1);
|
||||
resetQuickSetSelectState();
|
||||
};
|
||||
|
||||
const closeSmartCutoutTool = () => {
|
||||
runSmartCutoutPageTransition(
|
||||
{
|
||||
@@ -3312,6 +3430,17 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
const removeCloneReferenceImage = (imageId: string) => {
|
||||
setCloneReferenceImages((current) => {
|
||||
const next = current.filter((item) => item.id !== imageId);
|
||||
if (next.length === 0) {
|
||||
setHotStatus("idle");
|
||||
setHotResultUrl(null);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloneReferenceUpload = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (!files?.length) return;
|
||||
@@ -3416,23 +3545,29 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const normalizedPlatform = normalizePlatform(nextPlatform);
|
||||
setPlatform(normalizedPlatform);
|
||||
setRatio((current) =>
|
||||
cloneOutput === "hot" && current.startsWith("上传图片") && hotUploadedRatioOption
|
||||
? hotUploadedRatioOption
|
||||
: normalizeRatioForPlatform(normalizedPlatform, current, cloneOutput),
|
||||
normalizeRatioForPlatform(normalizedPlatform, current, cloneOutput),
|
||||
);
|
||||
setLanguage(getPlatformDefaultLanguage(normalizedPlatform, market));
|
||||
};
|
||||
|
||||
const handleCloneOutputChange = (nextOutput: CloneOutputKey) => {
|
||||
setCloneOutput(nextOutput);
|
||||
setIsCloneTemplateStripVisible(true);
|
||||
if (nextOutput !== "video") setIsVideoWorkspaceVisible(false);
|
||||
setRatio((current) =>
|
||||
nextOutput === "hot" && current.startsWith("上传图片") && hotUploadedRatioOption
|
||||
? hotUploadedRatioOption
|
||||
: normalizeRatioForPlatform(platform, current, nextOutput),
|
||||
normalizeRatioForPlatform(platform, current, nextOutput),
|
||||
);
|
||||
};
|
||||
|
||||
const handleCloneModeTabClick = (nextOutput: CloneOutputKey) => {
|
||||
if (nextOutput === cloneOutput) {
|
||||
setIsCloneTemplateStripVisible((visible) => !visible);
|
||||
return;
|
||||
}
|
||||
handleCloneOutputChange(nextOutput);
|
||||
setComposerMenu(null);
|
||||
};
|
||||
|
||||
const handleCloneMarketChange = (nextMarket: string) => {
|
||||
const normalizedMarket = normalizeMarket(nextMarket);
|
||||
setMarket(normalizedMarket);
|
||||
@@ -3568,14 +3703,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
useEffect(() => {
|
||||
setRatio((current) => {
|
||||
const platformRatios = getPlatformRatioOptions(platform, cloneOutput);
|
||||
const availableRatios = hotUploadedRatioOption ? getUniqueRatioOptions([...platformRatios, hotUploadedRatioOption]) : platformRatios;
|
||||
if (current.startsWith("上传图片") && hotUploadedRatioOption) return hotUploadedRatioOption;
|
||||
if (availableRatios.includes(current)) return current;
|
||||
if (platformRatios.includes(current)) return current;
|
||||
const normalizedRatio = normalizeRatioToken(current);
|
||||
const matchedRatio = availableRatios.find((option) => normalizeRatioToken(option).includes(normalizedRatio));
|
||||
const matchedRatio = platformRatios.find((option) => normalizeRatioToken(option).includes(normalizedRatio));
|
||||
return matchedRatio ?? getPlatformDefaultRatio(platform, cloneOutput);
|
||||
});
|
||||
}, [cloneOutput, hotUploadedRatioOption, platform]);
|
||||
}, [cloneOutput, platform]);
|
||||
|
||||
useEffect(() => {
|
||||
if (skipInitialCloneAutoSaveRef.current) {
|
||||
@@ -4360,6 +4493,133 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
);
|
||||
};
|
||||
|
||||
const handleHotPlatformChange = (nextPlatform: string) => {
|
||||
const normalizedPlatform = normalizePlatform(nextPlatform);
|
||||
setHotPlatform(normalizedPlatform);
|
||||
setHotLanguage(getPlatformDefaultLanguage(normalizedPlatform, hotMarket));
|
||||
setHotRatio((current) => getQuickSetRatioValue(current));
|
||||
};
|
||||
|
||||
const handleHotMarketChange = (nextMarket: string) => {
|
||||
const normalizedMarket = normalizeMarket(nextMarket);
|
||||
setHotMarket(normalizedMarket);
|
||||
setHotLanguage(getPlatformDefaultLanguage(hotPlatform, normalizedMarket));
|
||||
};
|
||||
|
||||
const handleHotAiWrite = () => {
|
||||
setHotRequirement(
|
||||
"1.产品名称:便携式咖啡保温杯\n2.核心卖点:316不锈钢内胆、12小时长效保温、防漏便携、大容量\n3.参考风格:极简日系、暖光氛围、生活场景\n4.期望场景:办公桌面、户外通勤、运动健身\n5.具体参数:容量500ml、口径4.5cm、高度22cm",
|
||||
);
|
||||
};
|
||||
|
||||
const stopHotProgress = () => {
|
||||
if (hotProgressRef.current !== null) {
|
||||
window.clearInterval(hotProgressRef.current);
|
||||
hotProgressRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startHotProgress = () => {
|
||||
stopHotProgress();
|
||||
setHotProgress(0);
|
||||
hotProgressRef.current = window.setInterval(() => {
|
||||
setHotProgress((prev) => {
|
||||
if (prev >= 90) {
|
||||
stopHotProgress();
|
||||
return 90;
|
||||
}
|
||||
return prev + (90 - prev) * 0.06;
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleHotGenerate = () => {
|
||||
if (!canGenerateHot) return;
|
||||
imageAbortRef.current = { current: false };
|
||||
lastFailedActionRef.current = null;
|
||||
startHotProgress();
|
||||
void generateEcommerceImage(
|
||||
"hot", cloneReferenceImages, hotRequirement,
|
||||
hotPlatform, hotRatio, hotLanguage, hotMarket,
|
||||
undefined,
|
||||
(s: string) => {
|
||||
setHotStatus(s as DetailStatus);
|
||||
if (s === "done") {
|
||||
stopHotProgress();
|
||||
setHotProgress(100);
|
||||
} else if (s === "failed" || s === "idle") {
|
||||
stopHotProgress();
|
||||
setHotProgress(0);
|
||||
}
|
||||
},
|
||||
(res) => setHotResultUrl(res[0]?.src ?? null),
|
||||
);
|
||||
};
|
||||
|
||||
const handleHotMaterialMouseEnter = (src: string, event: ReactMouseEvent<HTMLElement>) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const previewHalfWidth = 150;
|
||||
const previewHeight = 360;
|
||||
const gap = 12;
|
||||
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||
const x = Math.min(
|
||||
Math.max(rect.left + rect.width / 2, previewHalfWidth + gap),
|
||||
Math.max(previewHalfWidth + gap, viewportWidth - previewHalfWidth - gap),
|
||||
);
|
||||
const showAbove = rect.top > previewHeight + gap;
|
||||
const y = showAbove
|
||||
? rect.top - gap
|
||||
: Math.min(rect.bottom + gap, viewportHeight - gap);
|
||||
setHotMaterialHoverZoom({ src, x, y, placement: showAbove ? "above" : "below" });
|
||||
};
|
||||
const handleHotMaterialMouseLeave = () => setHotMaterialHoverZoom(null);
|
||||
|
||||
const renderHotMaterialThumbs = (items: CloneImageItem[], onRemove: (imageId: string) => void) => (
|
||||
<div className="ecom-quick-upload-thumbs" aria-label="已上传商品素材">
|
||||
{items.map((item) => (
|
||||
<figure
|
||||
key={item.id}
|
||||
className="ecom-command-asset-thumb ecom-quick-upload-thumb"
|
||||
onMouseEnter={(e) => handleHotMaterialMouseEnter(item.src, e)}
|
||||
onMouseLeave={handleHotMaterialMouseLeave}
|
||||
>
|
||||
<img src={item.src} alt={item.name} />
|
||||
<button
|
||||
type="button"
|
||||
className="ecom-hot-material-delete"
|
||||
aria-label={`删除${item.name || "图片"}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setHotMaterialHoverZoom(null);
|
||||
onRemove(item.id);
|
||||
}}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M9 6V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v1" />
|
||||
<path d="M5 6h14" />
|
||||
<path d="M8 6l1 14h6l1-14" />
|
||||
<path d="M10.5 10v6" />
|
||||
<path d="M13.5 10v6" />
|
||||
</svg>
|
||||
</button>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const closeHotClonePage = () => {
|
||||
stopHotProgress();
|
||||
setActiveQuickTool(null);
|
||||
setHotStatus("idle");
|
||||
setHotResultUrl(null);
|
||||
setHotProgress(0);
|
||||
setHotRequirement("");
|
||||
setIsHotMaterialDragging(false);
|
||||
setHotMaterialHoverZoom(null);
|
||||
setComposerMenu(null);
|
||||
};
|
||||
|
||||
const resetTask = () => {
|
||||
setSetImages([]);
|
||||
setProductSetRequirement("");
|
||||
@@ -4422,6 +4682,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const isWatermarkTool = isCloneTool && activeQuickTool === "watermark";
|
||||
const isImageEditTool = isCloneTool && activeQuickTool === "image-edit";
|
||||
const isTranslateTool = isCloneTool && activeQuickTool === "translate";
|
||||
const isHotCloneTool = isCloneTool && activeQuickTool === "hot";
|
||||
const pageLabel = isSetTool ? "商品套图" : isDetail ? "A+/详情页" : isTryOn ? "AI服饰穿戴" : activeToolMeta?.label || "商品工具";
|
||||
const setPrimaryLabel =
|
||||
setImages.length === 0
|
||||
@@ -4834,6 +5095,19 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
{ key: "ratio", label: "尺寸/比例", value: getQuickSetRatioValue(detailRatio), options: quickSetRatioOptions, onChange: setDetailRatio },
|
||||
];
|
||||
|
||||
const quickHotBasicSelects: Array<{
|
||||
key: CloneBasicSelectKey;
|
||||
label: string;
|
||||
value: string;
|
||||
options: string[];
|
||||
onChange: (value: string) => void;
|
||||
}> = [
|
||||
{ key: "platform", label: "平台", value: hotPlatform, options: platformOptions, onChange: handleHotPlatformChange },
|
||||
{ key: "market", label: "国家", value: hotMarket, options: marketOptions, onChange: handleHotMarketChange },
|
||||
{ key: "language", label: "语种", value: hotLanguage, options: hotLanguageOptions, onChange: setHotLanguage },
|
||||
{ key: "ratio", label: "尺寸/比例", value: getQuickSetRatioValue(hotRatio), options: quickSetRatioOptions, onChange: setHotRatio },
|
||||
];
|
||||
|
||||
const cloneModelSelects: Array<{
|
||||
key: CloneModelSelectKey;
|
||||
label: string;
|
||||
@@ -5113,8 +5387,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
? cloneModelPanelTab === "scene" ? "场景设置" : "模特设置"
|
||||
: cloneOutput === "video"
|
||||
? String(cloneVideoDuration) + "秒 " + (cloneVideoQuality === "standard" ? "720P" : "1080P")
|
||||
: cloneOutput === "hot"
|
||||
? cloneReplicateLevel === "style" ? "风格复刻" : "高度复刻"
|
||||
: "换装素材";
|
||||
|
||||
const renderComposerMenu = () => {
|
||||
@@ -5251,48 +5523,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
<input type="range" min={cloneVideoDurationMin} max={cloneVideoDurationMax} step={5} value={cloneVideoDuration} onChange={(event) => setCloneVideoDuration(clampCloneVideoDuration(Number(event.target.value)))} />
|
||||
</label>
|
||||
</>
|
||||
) : cloneOutput === "hot" ? (
|
||||
<>
|
||||
<header><strong>爆款复刻设置</strong><span>{cloneReferenceImages.length}/{maxCloneReferenceImages}</span></header>
|
||||
<div className="ecom-command-hot-layout">
|
||||
<button
|
||||
type="button"
|
||||
className={`ecom-command-hot-upload${isCloneReferenceDragging ? " is-dragging" : ""}${cloneReferenceImages.length ? " has-image" : ""}`}
|
||||
onClick={() => cloneReferenceInputRef.current?.click()}
|
||||
onDragOver={handleCloneReferenceDragOver}
|
||||
onDragLeave={handleCloneReferenceDragLeave}
|
||||
onDrop={handleCloneReferenceDrop}
|
||||
>
|
||||
{cloneReferenceImages[0]?.src ? (
|
||||
<>
|
||||
<span className="ecom-command-hot-thumb-grid">
|
||||
{cloneReferenceImages.map((image, index) => (
|
||||
<span key={image.id} className="ecom-command-hot-thumb">
|
||||
<img src={image.src} alt={`参考图 ${index + 1}`} />
|
||||
<span className="ecom-command-hot-zoom" aria-hidden="true">
|
||||
<img src={image.src} alt="" />
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<span>已上传 {cloneReferenceImages.length} 张,点击继续上传</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<strong>+ 上传参考图片</strong>
|
||||
<span>支持拖拽上传</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<div className="ecom-command-hot-levels">
|
||||
{cloneReplicateLevelOptions.map((option) => (
|
||||
<button key={option.key} type="button" className={cloneReplicateLevel === option.key ? "is-active" : ""} onClick={() => setCloneReplicateLevel(option.key)}>
|
||||
<strong>{option.title}</strong><span>{option.desc}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<header><strong>视频换装设置</strong><span>上传视频和服装参考</span></header>
|
||||
@@ -5364,6 +5594,60 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
toast.success("提示词已填入指令栏");
|
||||
};
|
||||
|
||||
const applyComposerPrompt = (prompt: string) => {
|
||||
const nextValue = prompt.slice(0, 500);
|
||||
setActiveQuickTool(null);
|
||||
setComposerMenu(null);
|
||||
setRequirement(nextValue);
|
||||
syncRequirementMentionQuery(nextValue, nextValue.length);
|
||||
setInspirationPreview(null);
|
||||
requestAnimationFrame(() => {
|
||||
const textarea = requirementTextareaRef.current;
|
||||
if (textarea) {
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(nextValue.length, nextValue.length);
|
||||
textarea.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addTemplateImageToComposer = async (card: CloneTemplateAsset) => {
|
||||
if (productImages.length >= maxCloneProductImages) {
|
||||
toast.info("模板图片已达上限");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stamp = Date.now();
|
||||
const uploaded = await aiGenerationClient.uploadAssetByUrl({
|
||||
sourceUrl: card.mediaUrl,
|
||||
name: `${card.id}-${stamp}`,
|
||||
scope: ecommerceOssScopes.productSource,
|
||||
});
|
||||
const nextImage: CloneImageItem = {
|
||||
id: `template-${card.id}-${stamp}`,
|
||||
src: uploaded.url || card.mediaUrl,
|
||||
name: card.title,
|
||||
ossKey: uploaded.ossKey,
|
||||
};
|
||||
setProductImages((current) => [...current, nextImage].slice(0, maxCloneProductImages));
|
||||
void readImageDimensions(nextImage.src)
|
||||
.then(({ width, height }) => {
|
||||
setProductImages((current) =>
|
||||
current.map((item) => (item.id === nextImage.id ? { ...item, width, height } : item)),
|
||||
);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
} catch {
|
||||
toast.error("模板图片导入失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloneTemplateCardClick = (card: CloneTemplateAsset) => {
|
||||
void addTemplateImageToComposer(card);
|
||||
applyComposerPrompt(card.prompt);
|
||||
};
|
||||
|
||||
const inspirationPreviewOverlay =
|
||||
inspirationPreview && typeof document !== "undefined"
|
||||
? createPortal(
|
||||
@@ -5702,7 +5986,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
key={option.key}
|
||||
type="button"
|
||||
className={cloneOutput === option.key ? "is-active" : ""}
|
||||
onClick={() => handleCloneOutputChange(option.key)}
|
||||
onClick={() => handleCloneModeTabClick(option.key)}
|
||||
>
|
||||
<span className={`ecom-command-mode-icon ecom-command-mode-icon--${option.key}`} aria-hidden="true">{option.icon}</span>
|
||||
<strong>{option.label}</strong>
|
||||
@@ -5801,10 +6085,30 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</div>
|
||||
{renderComposerMenu()}
|
||||
</div>
|
||||
{(status === "idle" || status === "ready") && !showMainVideoWorkspace && isCloneTemplateStripVisible ? (
|
||||
<section className={`ecom-command-template-strip ecom-command-template-strip--${cloneOutput}`} aria-label="模板卡片">
|
||||
{activeCloneTemplateCards.map((card) => (
|
||||
<button
|
||||
key={card.id}
|
||||
type="button"
|
||||
className="ecom-command-template-card"
|
||||
aria-label={card.title}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleCloneTemplateCardClick(card);
|
||||
}}
|
||||
>
|
||||
<span className="ecom-command-template-card__blank" aria-hidden="true" />
|
||||
</button>
|
||||
))}
|
||||
</section>
|
||||
) : null}
|
||||
{(status === "idle" || status === "ready") && !showMainVideoWorkspace ? (
|
||||
<section className="ecom-command-quick-board" aria-label="快捷功能">
|
||||
{[
|
||||
{ label: "A+/详情页", tone: "detail", icon: <LayoutOutlined />, onClick: openQuickDetailPage },
|
||||
{ label: "爆款复刻", tone: "hot", icon: <FireOutlined />, onClick: openHotClonePage },
|
||||
{ label: "图片修改", tone: "edit", icon: <EditOutlined />, onClick: openImageWorkbenchPage },
|
||||
{ label: "智能抠图", tone: "cutout", icon: <ScissorOutlined />, onClick: openSmartCutoutUpload },
|
||||
{ label: "去除水印", tone: "watermark", icon: <ClearOutlined />, onClick: openWatermarkRemovalPage },
|
||||
@@ -6822,6 +7126,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
);
|
||||
|
||||
const quickDetailVisibleSelect = quickDetailBasicSelects.find((item) => item.key === visibleQuickSetSelect) ?? null;
|
||||
const quickHotVisibleSelect = quickHotBasicSelects.find((item) => item.key === visibleQuickSetSelect) ?? null;
|
||||
|
||||
const quickDetailPreview = (
|
||||
<main key="quick-detail" className={`ecom-quick-set-page ecom-quick-detail-page ecom-tool-page-enter${isQuickPanelCollapsed ? " is-panel-collapsed" : ""}`} aria-label="A+详情页生成">
|
||||
@@ -7019,6 +7324,270 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</main>
|
||||
);
|
||||
|
||||
const hotClonePreview = (
|
||||
<main key="quick-hot" className={`ecom-quick-set-page ecom-quick-hot-page ecom-tool-page-enter${isQuickPanelCollapsed ? " is-panel-collapsed" : ""}`} aria-label="爆款复刻生成">
|
||||
<div className="ecom-quick-set-body">
|
||||
<aside className="ecom-quick-set-panel" aria-label="爆款复刻设置" onWheel={handleQuickPanelWheel}>
|
||||
<header className="ecom-quick-set-panel-head">
|
||||
<strong className="ecom-quick-set-page-title">爆款复刻</strong>
|
||||
<button type="button" className="ecom-quick-set-back" onClick={closeHotClonePage}>
|
||||
首页
|
||||
</button>
|
||||
<button type="button" className="ecom-quick-set-back" onClick={closeHotClonePage}>
|
||||
上一页
|
||||
</button>
|
||||
</header>
|
||||
<section>
|
||||
<strong><FileImageOutlined /> 上传素材</strong>
|
||||
{productImages.length ? (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`ecom-quick-set-upload ecom-quick-hot-material has-images${isHotMaterialDragging ? " is-dragging" : ""}`}
|
||||
onClick={() => hotMaterialInputRef.current?.click()}
|
||||
onKeyDown={(event) => openQuickUploadWithKeyboard(event, hotMaterialInputRef)}
|
||||
onDragOver={(event) => { event.preventDefault(); event.stopPropagation(); if (event.dataTransfer.types.includes("Files")) setIsHotMaterialDragging(true); }}
|
||||
onDragLeave={(event) => { event.preventDefault(); event.stopPropagation(); if (event.currentTarget === event.target || !event.currentTarget.contains(event.relatedTarget as Node)) setIsHotMaterialDragging(false); }}
|
||||
onDrop={(event) => { event.preventDefault(); event.stopPropagation(); setIsHotMaterialDragging(false); const files = Array.from(event.dataTransfer.files); if (files.length) addProductImages(files); }}
|
||||
>
|
||||
{renderHotMaterialThumbs(productImages, removeProductImage)}
|
||||
<button
|
||||
type="button"
|
||||
className="ecom-quick-hot-add-btn"
|
||||
aria-label="添加更多素材"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
hotMaterialInputRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`ecom-quick-set-upload ecom-quick-hot-material${isHotMaterialDragging ? " is-dragging" : ""}`}
|
||||
onClick={() => hotMaterialInputRef.current?.click()}
|
||||
onKeyDown={(event) => openQuickUploadWithKeyboard(event, hotMaterialInputRef)}
|
||||
onDragOver={(event) => { event.preventDefault(); event.stopPropagation(); if (event.dataTransfer.types.includes("Files")) setIsHotMaterialDragging(true); }}
|
||||
onDragLeave={(event) => { event.preventDefault(); event.stopPropagation(); if (event.currentTarget === event.target || !event.currentTarget.contains(event.relatedTarget as Node)) setIsHotMaterialDragging(false); }}
|
||||
onDrop={(event) => { event.preventDefault(); event.stopPropagation(); setIsHotMaterialDragging(false); const files = Array.from(event.dataTransfer.files); if (files.length) addProductImages(files); }}
|
||||
>
|
||||
<FileImageOutlined />
|
||||
<span>拖拽或点击上传</span>
|
||||
<em>上传商品素材图,最多 {maxCloneProductImages} 张</em>
|
||||
<b>+ 上传图片</b>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={hotMaterialInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="ecom-command-hidden-file"
|
||||
onChange={handleProductUpload}
|
||||
aria-label="上传爆款复刻素材"
|
||||
/>
|
||||
</section>
|
||||
<section>
|
||||
<strong><FireOutlined /> 上传参考图片</strong>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`ecom-quick-set-upload ecom-quick-hot-material ecom-quick-hot-reference${cloneReferenceImages.length ? " has-images" : ""}${isCloneReferenceDragging ? " is-dragging" : ""}`}
|
||||
onClick={() => cloneReferenceInputRef.current?.click()}
|
||||
onKeyDown={(event) => openQuickUploadWithKeyboard(event, cloneReferenceInputRef)}
|
||||
onDragOver={(event) => { event.preventDefault(); event.stopPropagation(); if (event.dataTransfer.types.includes("Files")) setIsCloneReferenceDragging(true); }}
|
||||
onDragLeave={(event) => { event.preventDefault(); event.stopPropagation(); if (event.currentTarget === event.target || !event.currentTarget.contains(event.relatedTarget as Node)) setIsCloneReferenceDragging(false); }}
|
||||
onDrop={(event) => { event.preventDefault(); event.stopPropagation(); setIsCloneReferenceDragging(false); const files = Array.from(event.dataTransfer.files); if (files.length) addCloneReferenceImages(files); }}
|
||||
>
|
||||
<FileImageOutlined />
|
||||
<span>拖拽或点击上传</span>
|
||||
<em>参考图用于风格迁移,最多 {maxCloneReferenceImages} 张</em>
|
||||
<b>+ 上传图片</b>
|
||||
{cloneReferenceImages.length ? (
|
||||
<>
|
||||
{renderHotMaterialThumbs(cloneReferenceImages, removeCloneReferenceImage)}
|
||||
<button
|
||||
type="button"
|
||||
className="ecom-quick-hot-add-btn"
|
||||
aria-label="添加更多参考图"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
cloneReferenceInputRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<input
|
||||
ref={cloneReferenceInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="ecom-command-hidden-file"
|
||||
onChange={handleCloneReferenceUpload}
|
||||
aria-label="上传爆款复刻参考图"
|
||||
/>
|
||||
</section>
|
||||
<section>
|
||||
<strong>复刻强度</strong>
|
||||
<div className="ecom-quick-detail-modules">
|
||||
{cloneReplicateLevelOptions.map((option) => (
|
||||
<button
|
||||
key={option.key}
|
||||
type="button"
|
||||
className={cloneReplicateLevel === option.key ? "is-active" : ""}
|
||||
onClick={() => setCloneReplicateLevel(option.key)}
|
||||
>
|
||||
<strong>{option.title}</strong>
|
||||
<span>{option.desc}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="ecom-quick-set-basic-section">
|
||||
<span className="ecom-quick-set-label">基础设置</span>
|
||||
<div className="ecom-quick-set-select-anchor">
|
||||
<div className="ecom-quick-set-selects">
|
||||
{quickHotBasicSelects.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={openQuickSetSelect === item.key ? "is-active" : ""}
|
||||
onClick={() => toggleQuickSetSelect(item.key)}
|
||||
>
|
||||
<span>{item.label}</span><strong>{formatRatioDisplayValue(item.value)}</strong><em>⌄</em>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{quickHotVisibleSelect ? (
|
||||
<div
|
||||
className={`ecom-quick-set-dropdown ecom-quick-set-dropdown--${quickHotVisibleSelect.key}${isQuickSetSelectClosing ? " is-closing" : ""}`}
|
||||
role="listbox"
|
||||
aria-label={quickHotVisibleSelect.label}
|
||||
>
|
||||
{quickHotVisibleSelect.options.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
className={quickHotVisibleSelect.value === option ? "is-active" : ""}
|
||||
onClick={() => {
|
||||
quickHotVisibleSelect.onChange(option);
|
||||
closeQuickSetSelect();
|
||||
}}
|
||||
>
|
||||
{formatRatioDisplayValue(option)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
<section className="ecom-quick-hot-requirement">
|
||||
<div className="ecom-quick-hot-requirement__head">
|
||||
<strong>商品卖点 & 需求</strong>
|
||||
<button type="button" className="ecom-quick-hot-requirement__ai" onClick={handleHotAiWrite}>AI 帮写</button>
|
||||
</div>
|
||||
<div className="ecom-quick-hot-requirement__input">
|
||||
<textarea
|
||||
value={hotRequirement}
|
||||
onChange={(event) => setHotRequirement(event.target.value.slice(0, 500))}
|
||||
placeholder="建议包含以下信息:产品名称、核心卖点、参考风格、期望场景、具体参数"
|
||||
maxLength={500}
|
||||
/>
|
||||
<span>{hotRequirement.length}/500</span>
|
||||
</div>
|
||||
</section>
|
||||
<div className="ecom-quick-hot-actions">
|
||||
<button type="button" className="ecom-quick-set-primary ecom-quick-hot-generate" onClick={handleHotGenerate} disabled={!canGenerateHot}>
|
||||
{hotStatus === "generating" ? <LoadingOutlined /> : "✦"} 开始复刻
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`ecom-quick-set-primary ecom-quick-set-primary--cancel${hotStatus !== "generating" ? " is-disabled" : ""}`}
|
||||
onClick={hotStatus === "generating" ? handleCancelGenerate : undefined}
|
||||
disabled={hotStatus !== "generating"}
|
||||
>
|
||||
取消复刻
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
{hotMaterialHoverZoom && typeof document !== "undefined"
|
||||
? createPortal(
|
||||
<div
|
||||
className={`ecom-hot-material-zoom-portal is-${hotMaterialHoverZoom.placement}`}
|
||||
style={{ left: hotMaterialHoverZoom.x, top: hotMaterialHoverZoom.y }}
|
||||
>
|
||||
<img src={hotMaterialHoverZoom.src} alt="" />
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
: null}
|
||||
<section className="ecom-quick-set-stage">
|
||||
<header className="ecom-quick-set-preview-head">
|
||||
<h1>预览</h1>
|
||||
<p>上传参考图,AI 按选定风格强度 <span>复刻同款视觉表现</span>,快速产出高转化素材。</p>
|
||||
<div>
|
||||
<button type="button" onClick={() => setPreviewZoom((value) => Math.max(0.25, value - 0.1))}>-</button>
|
||||
<strong>{Math.round(previewZoom * 100)}%</strong>
|
||||
<button type="button" onClick={() => setPreviewZoom((value) => Math.min(2, value + 0.1))}>+</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="ecom-quick-set-canvas" onWheel={handleQuickPreviewWheel}>
|
||||
{hotStatus === "done" && hotResultUrl ? (
|
||||
<section className="ecom-quick-detail-result" style={{ transform: `scale(${previewZoom})` }}>
|
||||
<img src={hotResultUrl} alt="爆款复刻结果" />
|
||||
<button
|
||||
type="button"
|
||||
className="ecom-quick-detail-download"
|
||||
onClick={() => {
|
||||
const link = document.createElement("a");
|
||||
link.href = hotResultUrl;
|
||||
link.download = `爆款复刻-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}}
|
||||
>
|
||||
<CloudUploadOutlined />
|
||||
保存本地
|
||||
</button>
|
||||
</section>
|
||||
) : hotStatus === "generating" ? (
|
||||
<section className="ecom-quick-set-generating">
|
||||
<LoadingOutlined />
|
||||
<strong>正在生成爆款复刻</strong>
|
||||
<span>AI 正在根据参考图和复刻强度生成同款素材,请稍候...</span>
|
||||
<div className="ecom-quick-set-progress">
|
||||
<div className="ecom-quick-set-progress-bar" style={{ width: `${Math.round(hotProgress)}%` }} />
|
||||
</div>
|
||||
<em className="ecom-quick-set-progress-text">{Math.round(hotProgress)}%</em>
|
||||
</section>
|
||||
) : hotStatus === "failed" ? (
|
||||
<section className="ecom-quick-set-failed">
|
||||
<FrownOutlined />
|
||||
<strong>生成失败</strong>
|
||||
<span>请检查网络或重试,如余额不足请先充值。</span>
|
||||
<button type="button" onClick={handleHotGenerate} disabled={!canGenerateHot}>重新生成</button>
|
||||
</section>
|
||||
) : (
|
||||
<section className="ecom-quick-set-empty">
|
||||
<FileImageOutlined />
|
||||
<strong>等待生成</strong>
|
||||
<span>上传参考图片并选择复刻强度后,AI 将在这里展示生成结果。</span>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<button type="button" className="ecom-quick-set-help" aria-label="帮助" onClick={() => toast.info("上传参考图后,选择复刻强度和平台即可生成爆款同款。")}>?</button>
|
||||
</main>
|
||||
);
|
||||
|
||||
const detailPreview = (
|
||||
<main className="product-clone-preview product-clone-preview--detail" aria-label="A+详情预览" onWheel={handlePreviewWheel}>
|
||||
<div className="product-clone-preview__headline">
|
||||
@@ -7135,6 +7704,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
{quickDetailPreview}
|
||||
</div>
|
||||
)
|
||||
: isHotCloneTool
|
||||
? (
|
||||
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
||||
{hotClonePreview}
|
||||
</div>
|
||||
)
|
||||
: clonePreview
|
||||
: placeholderPreview;
|
||||
const isMainCloneWorkspace = isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool;
|
||||
@@ -7173,7 +7748,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`product-clone-page page-motion${isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : ""}${isCloneTool && isCommandHistoryCollapsed ? " is-history-collapsed" : ""}${isRecordDetailWorkspace && isCloneConversationCollapsed ? " is-conversation-collapsed" : ""}${isRecordDetailWorkspace ? " is-history-detail" : ""}${isSmartCutoutTool ? " is-smart-cutout-page" : ""}${isQuickDetailTool ? " is-quick-set-page" : ""}${isWatermarkTool ? " is-watermark-page" : ""}${isTranslateTool ? " is-translate-page" : ""}${isImageEditTool ? " is-image-workbench-page" : ""}`}
|
||||
className={`product-clone-page page-motion${isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : ""}${isCloneTool && isCommandHistoryCollapsed ? " is-history-collapsed" : ""}${isRecordDetailWorkspace && isCloneConversationCollapsed ? " is-conversation-collapsed" : ""}${isRecordDetailWorkspace ? " is-history-detail" : ""}${isSmartCutoutTool ? " is-smart-cutout-page" : ""}${isQuickDetailTool ? " is-quick-set-page" : ""}${isWatermarkTool ? " is-watermark-page" : ""}${isTranslateTool ? " is-translate-page" : ""}${isImageEditTool ? " is-image-workbench-page" : ""}${isHotCloneTool ? " is-hot-clone-page" : ""}`}
|
||||
data-tool={activeTool}
|
||||
aria-label={pageLabel}
|
||||
>
|
||||
@@ -7197,7 +7772,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
{isSetTool ? setPanel : isDetail ? detailPanel : isTryOn ? tryOnPanel : isCloneTool ? clonePanel : placeholderPanel}
|
||||
</aside>
|
||||
|
||||
{isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool ? (
|
||||
{isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool && !isHotCloneTool ? (
|
||||
<button
|
||||
type="button"
|
||||
className="clone-ai-settings-toggle"
|
||||
|
||||
Reference in New Issue
Block a user