diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0d06b26 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,43 @@ +# 自动检测文本文件并统一换行符 +* text=auto eol=lf + +# 源码强制使用 LF(跨平台一致) +*.ts text eol=lf +*.tsx text eol=lf +*.js text eol=lf +*.jsx text eol=lf +*.mjs text eol=lf +*.cjs text eol=lf +*.json text eol=lf +*.css text eol=lf +*.html text eol=lf +*.md text eol=lf +*.svg text eol=lf + +# 配置类(统一 LF) +*.yml text eol=lf +*.yaml text eol=lf +*.toml text eol=lf +*.conf text eol=lf + +# Windows 专用脚本保持 CRLF +*.bat text eol=crlf +*.cmd text eol=crlf + +# 二进制文件,不做换行符转换 +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.webp binary +*.ico binary +*.woff binary +*.woff2 binary +*.ttf binary +*.eot binary +*.otf binary +*.mp4 binary +*.mp3 binary +*.pdf binary +*.zip binary +*.gz binary diff --git a/src/api/aiGenerationClient.js b/src/api/aiGenerationClient.js new file mode 100644 index 0000000..29f16fb --- /dev/null +++ b/src/api/aiGenerationClient.js @@ -0,0 +1 @@ +export * from "./aiGenerationClient.ts"; diff --git a/src/api/apiErrorUtils.js b/src/api/apiErrorUtils.js new file mode 100644 index 0000000..45dda37 --- /dev/null +++ b/src/api/apiErrorUtils.js @@ -0,0 +1 @@ +export * from "./apiErrorUtils.ts"; diff --git a/src/api/generationRecordClient.js b/src/api/generationRecordClient.js new file mode 100644 index 0000000..7acc65c --- /dev/null +++ b/src/api/generationRecordClient.js @@ -0,0 +1 @@ +export * from "./generationRecordClient.ts"; diff --git a/src/api/serverConnection.js b/src/api/serverConnection.js new file mode 100644 index 0000000..39e1407 --- /dev/null +++ b/src/api/serverConnection.js @@ -0,0 +1 @@ +export * from "./serverConnection.ts"; diff --git a/src/api/taskSubscription.js b/src/api/taskSubscription.js new file mode 100644 index 0000000..a4e1d36 --- /dev/null +++ b/src/api/taskSubscription.js @@ -0,0 +1 @@ +export * from "./taskSubscription.ts"; diff --git a/src/api/webGenerationGateway.js b/src/api/webGenerationGateway.js new file mode 100644 index 0000000..b58a97f --- /dev/null +++ b/src/api/webGenerationGateway.js @@ -0,0 +1 @@ +export * from "./webGenerationGateway.ts"; diff --git a/src/components/toast/toastStore.js b/src/components/toast/toastStore.js new file mode 100644 index 0000000..0482274 --- /dev/null +++ b/src/components/toast/toastStore.js @@ -0,0 +1 @@ +export * from "./toastStore.ts"; diff --git a/src/data/ossAssets.js b/src/data/ossAssets.js new file mode 100644 index 0000000..8df1f4f --- /dev/null +++ b/src/data/ossAssets.js @@ -0,0 +1 @@ +export * from "./ossAssets.ts"; diff --git a/src/data/workflows.js b/src/data/workflows.js new file mode 100644 index 0000000..fb6a6b7 --- /dev/null +++ b/src/data/workflows.js @@ -0,0 +1 @@ +export * from "./workflows.ts"; diff --git a/src/features/ecommerce/EcommercePage.js b/src/features/ecommerce/EcommercePage.js new file mode 100644 index 0000000..ee7a3b9 --- /dev/null +++ b/src/features/ecommerce/EcommercePage.js @@ -0,0 +1,2 @@ +export { default } from "./EcommercePage.tsx"; +export * from "./EcommercePage.tsx"; diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index 0ff6c0d..ea7d2bd 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -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: }, { key: "video", label: "短视频", desc: "分镜视频链路", icon: }, ]; -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: }, ]; +const cloneTemplateCards: Record, 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(null); const detailInputRef = useRef(null); const detailProgressRef = useRef(null); + const hotProgressRef = useRef(null); + const hotMaterialInputRef = useRef(null); const countHoldTimeoutRef = useRef(null); const countHoldIntervalRef = useRef(null); const isAuthenticated = Boolean((_props as Record).isAuthenticated); @@ -1510,7 +1624,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const [selectedProductSetPreview, setSelectedProductSetPreview] = useState(null); const [showHostingModal, setShowHostingModal] = useState(false); const [productImages, setProductImages] = useState([]); - 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(null); const [smartCutoutBatchImages, setSmartCutoutBatchImages] = useState([]); 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(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("idle"); const [detailResultUrl, setDetailResultUrl] = useState(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("idle"); + const [hotResultUrl, setHotResultUrl] = useState(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) => { 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) => { + 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) => ( +
+ {items.map((item) => ( +
handleHotMaterialMouseEnter(item.src, e)} + onMouseLeave={handleHotMaterialMouseLeave} + > + {item.name} + +
+ ))} +
+ ); + + 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 = {}) { setCloneVideoDuration(clampCloneVideoDuration(Number(event.target.value)))} /> - ) : cloneOutput === "hot" ? ( - <> -
爆款复刻设置{cloneReferenceImages.length}/{maxCloneReferenceImages}
-
- -
- {cloneReplicateLevelOptions.map((option) => ( - - ))} -
-
- ) : ( <>
视频换装设置上传视频和服装参考
@@ -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)} > {option.label} @@ -5801,10 +6085,30 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { {renderComposerMenu()} + {(status === "idle" || status === "ready") && !showMainVideoWorkspace && isCloneTemplateStripVisible ? ( +
+ {activeCloneTemplateCards.map((card) => ( + + ))} +
+ ) : null} {(status === "idle" || status === "ready") && !showMainVideoWorkspace ? (
{[ { label: "A+/详情页", tone: "detail", icon: , onClick: openQuickDetailPage }, + { label: "爆款复刻", tone: "hot", icon: , onClick: openHotClonePage }, { label: "图片修改", tone: "edit", icon: , onClick: openImageWorkbenchPage }, { label: "智能抠图", tone: "cutout", icon: , onClick: openSmartCutoutUpload }, { label: "去除水印", tone: "watermark", icon: , 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 = (
@@ -7019,6 +7324,270 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
); + const hotClonePreview = ( +
+
+