@@ -282,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" ;
@@ -315,6 +321,21 @@ interface CanvasNode {
y : number ;
}
interface PreviewTouchPoint {
id : number ;
x : number ;
y : number ;
}
interface PreviewTouchGesture {
mode : "none" | "pan" | "pinch" ;
points : PreviewTouchPoint [ ] ;
startOffset : { x : number ; y : number } ;
startZoom : number ;
startDistance : number ;
startCenter : { x : number ; y : number } ;
}
interface CloneSavedSetting {
id : string ;
name : string ;
@@ -990,6 +1011,112 @@ const productSetOutputOptions: Array<{ key: ProductSetOutputKey; label: string;
const cloneOutputOptions : Array < { key : ProductSetOutputKey ; label : string ; desc : string ; icon : ReactNode } > = [
. . . productSetOutputOptions ,
] ;
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 ;
@@ -1007,7 +1134,7 @@ const defaultCloneSetCounts: Record<CloneSetCountKey, number> = {
} ;
const minCloneSetTotal = 1 ;
const maxCloneSetTotal = 16 ;
const maxCloneProductImages = 7 ;
const maxCloneProductImages = 20 ;
const maxCloneReferenceImages = 20 ;
const cloneVideoDurationMin = 5 ;
const cloneVideoDurationMax = 45 ;
@@ -1442,6 +1569,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 ) ;
@@ -1476,6 +1604,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const [ cloneVideoDuration , setCloneVideoDuration ] = useState ( 10 ) ;
const [ cloneVideoSmart , setCloneVideoSmart ] = useState ( true ) ;
const [ isCloneSettingsCollapsed , setIsCloneSettingsCollapsed ] = useState ( false ) ;
const [ isCloneConversationCollapsed , setIsCloneConversationCollapsed ] = useState ( false ) ;
const [ previewZoom , setPreviewZoom ] = useState ( 1 ) ;
const quickSetSelectTimerRef = useRef < number | null > ( null ) ;
const openQuickSetSelectRef = useRef < CloneBasicSelectKey | null > ( null ) ;
@@ -1495,6 +1624,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
offsetX : 0 ,
offsetY : 0 ,
} ) ;
const previewTouchGestureRef = useRef < PreviewTouchGesture > ( {
mode : "none" ,
points : [ ] ,
startOffset : { x : 0 , y : 0 } ,
startZoom : 1 ,
startDistance : 0 ,
startCenter : { x : 0 , y : 0 } ,
} ) ;
const nodeDragRef = useRef < { active : boolean ; nodeId : string ; startX : number ; startY : number ; originX : number ; originY : number } > ( {
active : false ,
nodeId : "" ,
@@ -1536,6 +1673,114 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
[ previewOffset . x , previewOffset . y , previewZoom ] ,
) ;
const updatePreviewTransform = ( nextZoom : number , nextOffset : { x : number ; y : number } ) = > {
previewZoomRef . current = nextZoom ;
previewOffsetRef . current = nextOffset ;
setPreviewZoom ( nextZoom ) ;
setPreviewOffset ( nextOffset ) ;
} ;
const getPreviewGestureDistance = ( points : PreviewTouchPoint [ ] ) = > {
if ( points . length < 2 ) return 0 ;
return Math . hypot ( points [ 0 ] ! . x - points [ 1 ] ! . x , points [ 0 ] ! . y - points [ 1 ] ! . y ) ;
} ;
const getPreviewGestureCenter = ( points : PreviewTouchPoint [ ] ) = > {
if ( points . length < 2 ) return points [ 0 ] ? { x : points [ 0 ] . x , y : points [ 0 ] . y } : { x : 0 , y : 0 } ;
return {
x : ( points [ 0 ] ! . x + points [ 1 ] ! . x ) / 2 ,
y : ( points [ 0 ] ! . y + points [ 1 ] ! . y ) / 2 ,
} ;
} ;
const isPreviewTouchInteractiveTarget = ( target : HTMLElement | null ) = >
Boolean ( target ? . closest ( ".ecom-command-composer-wrap, .clone-ai-preview-header, .clone-ai-source-corner-action, input, textarea, select, a, button" ) ) ;
const startPreviewTouchGesture = ( event : ReactPointerEvent < HTMLElement > ) = > {
if ( event . pointerType === "mouse" || isPreviewTouchInteractiveTarget ( event . target as HTMLElement | null ) ) return ;
event . preventDefault ( ) ;
event . currentTarget . setPointerCapture ( event . pointerId ) ;
const points = [
. . . previewTouchGestureRef . current . points . filter ( ( point ) = > point . id !== event . pointerId ) ,
{ id : event.pointerId , x : event.clientX , y : event.clientY } ,
] . slice ( - 2 ) ;
const mode = points . length >= 2 ? "pinch" : "pan" ;
previewTouchGestureRef . current = {
mode ,
points ,
startOffset : previewOffsetRef.current ,
startZoom : previewZoomRef.current ,
startDistance : getPreviewGestureDistance ( points ) ,
startCenter : getPreviewGestureCenter ( points ) ,
} ;
event . currentTarget . classList . add ( "is-touch-panning" ) ;
} ;
const movePreviewTouchGesture = ( event : ReactPointerEvent < HTMLElement > ) = > {
const gesture = previewTouchGestureRef . current ;
if ( gesture . mode === "none" || event . pointerType === "mouse" ) return ;
event . preventDefault ( ) ;
const points = gesture . points . map ( ( point ) = > point . id === event . pointerId ? { . . . point , x : event.clientX , y : event.clientY } : point ) ;
if ( ! points . some ( ( point ) = > point . id === event . pointerId ) ) return ;
if ( gesture . mode === "pinch" && points . length >= 2 && gesture . startDistance > 0 ) {
const rect = event . currentTarget . getBoundingClientRect ( ) ;
const center = getPreviewGestureCenter ( points ) ;
const zoomRatio = getPreviewGestureDistance ( points ) / gesture . startDistance ;
const nextZoom = Math . min ( 2 , Math . max ( 0.25 , gesture . startZoom * zoomRatio ) ) ;
const startCenterX = gesture . startCenter . x - rect . left ;
const startCenterY = gesture . startCenter . y - rect . top ;
const currentCenterX = center . x - rect . left ;
const currentCenterY = center . y - rect . top ;
const contentX = ( startCenterX - gesture . startOffset . x ) / gesture . startZoom ;
const contentY = ( startCenterY - gesture . startOffset . y ) / gesture . startZoom ;
updatePreviewTransform ( nextZoom , {
x : currentCenterX - contentX * nextZoom ,
y : currentCenterY - contentY * nextZoom ,
} ) ;
} else {
const point = points [ 0 ] ! ;
const startPoint = gesture . points [ 0 ] ! ;
updatePreviewTransform ( gesture . startZoom , {
x : gesture.startOffset.x + point . x - startPoint . x ,
y : gesture.startOffset.y + point . y - startPoint . y ,
} ) ;
}
previewTouchGestureRef . current = { . . . gesture , points } ;
} ;
const stopPreviewTouchGesture = ( event : ReactPointerEvent < HTMLElement > ) = > {
const gesture = previewTouchGestureRef . current ;
if ( event . pointerType === "mouse" || gesture . mode === "none" ) return ;
const points = gesture . points . filter ( ( point ) = > point . id !== event . pointerId ) ;
if ( points . length ) {
previewTouchGestureRef . current = {
mode : "pan" ,
points ,
startOffset : previewOffsetRef.current ,
startZoom : previewZoomRef.current ,
startDistance : 0 ,
startCenter : getPreviewGestureCenter ( points ) ,
} ;
} else {
previewTouchGestureRef . current = {
mode : "none" ,
points : [ ] ,
startOffset : previewOffsetRef.current ,
startZoom : previewZoomRef.current ,
startDistance : 0 ,
startCenter : { x : 0 , y : 0 } ,
} ;
event . currentTarget . classList . remove ( "is-touch-panning" ) ;
}
try {
event . currentTarget . releasePointerCapture ( event . pointerId ) ;
} catch {
// Pointer capture can already be released by the browser after cancel.
}
} ;
useEffect ( ( ) = > {
const container = previewSurfaceRef . current ;
if ( ! container ) return undefined ;
@@ -1645,8 +1890,43 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
onAuxClick : ( event : ReactMouseEvent < HTMLElement > ) = > {
if ( event . button === 1 ) event . preventDefault ( ) ;
} ,
onPointerDown : startPreviewTouchGesture ,
onPointerMove : movePreviewTouchGesture ,
onPointerUp : stopPreviewTouchGesture ,
onPointerCancel : stopPreviewTouchGesture ,
} ) ;
const startCanvasNodeDrag = ( event : ReactPointerEvent < HTMLElement > , node : CanvasNode ) = > {
if ( event . button !== 0 || event . pointerType === "mouse" ) return ;
if ( ( event . target as HTMLElement | null ) ? . closest ( "button, a, input, textarea, select" ) ) return ;
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
event . currentTarget . setPointerCapture ( event . pointerId ) ;
nodeDragRef . current = { active : true , nodeId : node.id , startX : event.clientX , startY : event.clientY , originX : node.x , originY : node.y } ;
} ;
const moveCanvasNodeDrag = ( event : ReactPointerEvent < HTMLElement > , nodeId : string ) = > {
const drag = nodeDragRef . current ;
if ( ! drag . active || drag . nodeId !== nodeId || event . pointerType === "mouse" ) return ;
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
const zoom = previewZoomRef . current ;
const dx = ( event . clientX - drag . startX ) / zoom ;
const dy = ( event . clientY - drag . startY ) / zoom ;
setCanvasNodes ( ( prev ) = > prev . map ( ( node ) = > node . id === nodeId ? { . . . node , x : drag.originX + dx , y : drag.originY + dy } : node ) ) ;
} ;
const stopCanvasNodeDrag = ( event : ReactPointerEvent < HTMLElement > , nodeId : string ) = > {
if ( nodeDragRef . current . nodeId !== nodeId || event . pointerType === "mouse" ) return ;
nodeDragRef . current = { . . . nodeDragRef . current , active : false } ;
event . stopPropagation ( ) ;
try {
event . currentTarget . releasePointerCapture ( event . pointerId ) ;
} catch {
// Pointer capture can already be released by the browser after cancel.
}
} ;
const handlePreviewWheel = ( event : React.WheelEvent < HTMLElement > ) = > {
if ( ! event . currentTarget ) return ;
const container = event . currentTarget as HTMLElement ;
@@ -1792,6 +2072,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"
? "建议包含以下信息:产品名称、核心卖点、期望场景、模特外貌描述(如小麦色皮肤、齐刘海、眼角有泪痣)、具体参数"
@@ -3160,12 +3441,22 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const handleCloneOutputChange = ( nextOutput : CloneOutputKey ) = > {
setCloneOutput ( nextOutput ) ;
setIsCloneTemplateStripVisible ( true ) ;
if ( nextOutput !== "video" ) setIsVideoWorkspaceVisible ( false ) ;
setRatio ( ( current ) = >
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 ) ;
@@ -5004,6 +5295,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 (
@@ -5056,6 +5401,9 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
< button type = "button" onClick = { ( ) = > setPreviewZoom ( ( z ) = > Math . max ( 0.25 , z - 0.1 ) ) } disabled = { previewZoom <= 0.25 } aria-label = "缩小" > - < / button >
< span > { Math . round ( previewZoom * 100 ) } % < / span >
< button type = "button" onClick = { ( ) = > setPreviewZoom ( ( z ) = > Math . min ( 2 , z + 0.1 ) ) } disabled = { previewZoom >= 2 } aria-label = "放大" > + < / button >
{ activeHistoryRecordId ? (
< button type = "button" onClick = { ( ) = > { setPreviewZoom ( 1 ) ; setPreviewOffset ( { x : 0 , y : 0 } ) ; } } aria-label = "重置画布" > 重 置 < / button >
) : null }
< / div >
< / header >
@@ -5204,6 +5552,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
data - mode = { node . mode }
data - node - id = { node . id }
style = { { transform : ` translate( ${ node . x } px, ${ node . y } px) ` } }
onPointerDown = { ( event ) = > startCanvasNodeDrag ( event , node ) }
onPointerMove = { ( event ) = > moveCanvasNodeDrag ( event , node . id ) }
onPointerUp = { ( event ) = > stopCanvasNodeDrag ( event , node . id ) }
onPointerCancel = { ( event ) = > stopCanvasNodeDrag ( event , node . id ) }
>
< div
className = "clone-ai-node-drag-handle"
@@ -5335,7 +5687,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 >
@@ -5344,19 +5696,26 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
< / div >
< div className = "clone-ai-input-wrapper ecom-command-composer" >
{ productImages . length ? (
< div className = "ecom-command-asset-popover" aria-label = "已上传素材" >
< div className = "ecom-command-asset-popover" aria-label = { ` 已上传素材, ${ productImages . length } / ${ maxCloneProductImages } 张 ` } >
< button
type = "button"
className = "ecom-command-asset-add"
onClick = { ( ) = > productImages . length < maxCloneProductImages && productInputRef . current ? . click ( ) }
disabled = { productImages . length >= maxCloneProductImages }
aria - label = { productImages . length >= maxCloneProductImages ? ` 最多上传 ${ maxCloneProductImages } 张素材 ` : "继续上传素材" }
title = { productImages . length >= maxCloneProductImages ? ` 最多上传 ${ maxCloneProductImages } 张素材 ` : ` 继续上传素材 ${ productImages . length } / ${ maxCloneProductImages } ` }
>
< span aria-hidden = "true" > + < / span >
< small > { productImages . length >= maxCloneProductImages ? "已满" : "上传" } < / small >
< / button >
{ productImages . map ( ( image ) = > (
< figure key = { image . id } className = "ecom-command-asset-thumb" >
< img src = { image . src } alt = { image . name || "上传图片" } / >
< span className = "ecom-command-asset-zoom" aria-hidden = "true" >
< img src = { image . src } alt = "" / >
< / span >
< button type = "button" onClick = { ( ) = > removeProductImage ( image . id ) } aria-label = "删除图片" >
< DeleteOutlined / >
< / button >
< / figure >
) ) }
< button type = "button" className = "ecom-command-asset-add" onClick = { ( ) = > productInputRef . current ? . click ( ) } aria-label = "继续上传" > + < / button >
< / div >
) : null }
< div className = "ecom-command-option-row ecom-command-option-row--settings" >
@@ -5427,6 +5786,25 @@ 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 = "快捷功能" >
{ [
@@ -6718,7 +7096,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
< div
role = "button"
tabIndex = { 0 }
className = { ` ecom-quick-set-upload ${ cloneReferenceImages . length ? " has-images" : "" } ${ isCloneReferenceDragging ? " is-dragging" : "" } ` }
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 ) ; } }
@@ -6729,7 +7107,22 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
< span > 拖 拽 或 点 击 上 传 < / span >
< em > 参 考 图 用 于 风 格 迁 移 , 最 多 { maxCloneReferenceImages } 张 < / em >
< b > + 上 传 图 片 < / b >
{ cloneReferenceImages . length ? renderQuickUploadThumbs ( cloneReferenceImages , removeCloneReferenceImage ) : null }
{ 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 }
@@ -7020,10 +7413,34 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
)
: clonePreview
: placeholderPreview ;
const isMainCloneWorkspace = isCloneTool && ! isSmartCutoutTool && ! isQuickDetailTool && ! isWatermarkTool && ! isTranslateTool && ! isImageEditTool ;
const isRecordDetailWorkspace = isMainCloneWorkspace && Boolean ( activeHistoryRecordId ) ;
const currentResultCount = canvasNodes . reduce ( ( count , node ) = > count + node . results . length , 0 ) ;
const activeHistoryRecord = activeHistoryRecordId ? ecommerceHistoryRecords . find ( ( record ) = > record . id === activeHistoryRecordId ) : null ;
const currentResultThumbs = canvasNodes . flatMap ( ( node ) = > node . results ) . slice ( 0 , 6 ) ;
const activeHistoryImageIds = new Set ( ( activeHistoryRecord ? . productImages ? ? [ ] ) . map ( ( image ) = > image . id ) ) ;
const historyConversationImages = activeHistoryRecord ? . productImages ? . length ? activeHistoryRecord.productImages : productImages ;
const newConversationImages = activeHistoryRecord ? productImages . filter ( ( image ) = > ! activeHistoryImageIds . has ( image . id ) ) : [ ] ;
const historyRequirementText = activeHistoryRecord ? . requirement ? . trim ( ) || requirement . trim ( ) ;
const newRequirementText = requirement . trim ( ) && requirement . trim ( ) !== historyRequirementText
? requirement . trim ( )
: "继续上传素材,准备下一轮生成。" ;
const historyRequirementMeta = [
{ label : "平台" , value : activeHistoryRecord?.platform || platform } ,
{ label : "语种" , value : activeHistoryRecord?.language || language } ,
{ label : "比例" , value : formatRatioDisplayValue ( activeHistoryRecord ? . ratio || ratio ) } ,
{ label : "设置" , value : composerSettingLabel } ,
] ;
const currentRequirementMeta = [
{ label : "平台" , value : platform } ,
{ label : "语种" , value : language } ,
{ label : "比例" , value : formatRatioDisplayValue ( ratio ) } ,
{ label : "设置" , value : composerSettingLabel } ,
] ;
return (
< section
className = { ` product-clone-page page-motion ${ isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : "" } ${ isCloneTool && isCommandHistoryCollapsed ? " is-history-collapsed" : "" } ${ isCloneTool && activeHistoryRecordId ? " 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" : "" } ` }
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 }
>
@@ -7061,6 +7478,108 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
< / button >
) : null }
{ isRecordDetailWorkspace ? (
< >
< aside className = "clone-ai-conversation-panel" aria-label = "AI 对话" >
< header className = "clone-ai-conversation-head" >
< div >
< strong > { activeHistoryRecord ? . title || "生成详情" } < / strong >
< span > { selectedCloneOutput . label } · { platform } · { language } < / span >
< / div >
< button
type = "button"
onClick = { ( ) = > setIsCloneConversationCollapsed ( true ) }
aria - label = "收起对话"
title = "收起对话"
>
< MenuFoldOutlined / >
< / button >
< / header >
< div className = "clone-ai-conversation-body" >
< section className = "clone-ai-chat-message clone-ai-chat-message--user" >
< span > 需 求 < / span >
< p > { historyRequirementText || "上传商品素材,描述你想生成的商品图、详情图、模特图或短视频。" } < / p >
< div className = "clone-ai-chat-meta" aria-label = "需求参数" >
{ historyRequirementMeta . map ( ( item ) = > (
< em key = { item . label } >
< span > { item . label } < / span >
< strong > { item . value } < / strong >
< / em >
) ) }
< / div >
{ historyConversationImages . length ? (
< div className = "clone-ai-chat-assets" aria-label = "已上传素材" >
{ historyConversationImages . slice ( 0 , 4 ) . map ( ( image ) = > (
< img key = { image . id } src = { image . src } alt = { image . name || "商品素材" } / >
) ) }
{ historyConversationImages . length > 4 ? < em > + { historyConversationImages . length - 4 } < / em > : null }
< / div >
) : null }
< / section >
< section className = { ` clone-ai-chat-message clone-ai-chat-message--assistant is- ${ status } ` } >
< span > 电 商 图 设 计 师 < / span >
< p >
{ status === "done" || currentResultCount > 0
? ` 已生成 ${ currentResultCount || results . length || productSetResultImages . filter ( Boolean ) . length } 张结果,可在画布中拖拽、缩放和预览。 `
: status === "generating"
? ` 正在为 ${ platform } / ${ market } 生成 ${ selectedCloneOutput . label } ,结果会自动出现在中间画布。 `
: status === "failed"
? "生成失败,请检查网络或参数后重试。"
: "我会根据商品图、平台规则和提示词整理生成任务。" }
< / p >
{ status === "generating" ? (
< EcommerceProgressBar status = "generating" progress = { generationProgress } onCancel = { handleCancelGenerate } label = { ` ${ selectedCloneOutput . label } 生成 ` } / >
) : null }
{ currentResultThumbs . length ? (
< div className = "clone-ai-chat-results" aria-label = "生成结果缩略图" >
{ currentResultThumbs . map ( ( item ) = > (
< button
key = { item . id }
type = "button"
onClick = { ( ) = > openProductSetPreview ( item ) }
aria - label = { ` 预览 ${ item . label } ` }
>
< img src = { item . src } alt = { item . label } / >
< / button >
) ) }
< / div >
) : null }
< / section >
{ newConversationImages . length ? (
< section className = "clone-ai-chat-message clone-ai-chat-message--user clone-ai-chat-message--followup" >
< span > 新 需 求 < / span >
< p > { newRequirementText } < / p >
< div className = "clone-ai-chat-meta" aria-label = "新需求参数" >
{ currentRequirementMeta . map ( ( item ) = > (
< em key = { item . label } >
< span > { item . label } < / span >
< strong > { item . value } < / strong >
< / em >
) ) }
< / div >
< div className = "clone-ai-chat-assets" aria-label = "新增素材" >
{ newConversationImages . slice ( 0 , 4 ) . map ( ( image ) = > (
< img key = { image . id } src = { image . src } alt = { image . name || "新增商品素材" } / >
) ) }
{ newConversationImages . length > 4 ? < em > + { newConversationImages . length - 4 } < / em > : null }
< / div >
< / section >
) : null }
< / div >
< / aside >
< button
type = "button"
className = "clone-ai-conversation-toggle"
onClick = { ( ) = > setIsCloneConversationCollapsed ( ( current ) = > ! current ) }
aria - label = { isCloneConversationCollapsed ? "展开对话" : "收起对话" }
title = { isCloneConversationCollapsed ? "展开对话" : "收起对话" }
aria - expanded = { ! isCloneConversationCollapsed }
>
{ isCloneConversationCollapsed ? < MenuUnfoldOutlined / > : < MenuFoldOutlined / > }
< / button >
< / >
) : null }
{ activePreview }
< / div >
@@ -7095,7 +7614,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
onClick = { refreshEcommerceHistory }
disabled = { isHistoryRefreshing }
>
↻
< ReloadOutlined / >
< / button >
< / div >
< div className = "ecom-command-history__heading" >