2026-06-02 12:38:01 +08:00
import {
AppstoreOutlined ,
CloudUploadOutlined ,
CloseOutlined ,
FileImageOutlined ,
2026-06-03 20:19:07 +08:00
FrownOutlined ,
2026-06-02 12:38:01 +08:00
LoadingOutlined ,
MenuFoldOutlined ,
MenuUnfoldOutlined ,
QuestionCircleOutlined ,
2026-06-03 20:19:07 +08:00
ReloadOutlined ,
2026-06-02 12:38:01 +08:00
SettingOutlined ,
SkinOutlined ,
} from "@ant-design/icons" ;
import { useEffect , useRef , useState , type CSSProperties , type ChangeEvent , type DragEvent , type ReactNode } from "react" ;
2026-06-02 17:37:51 +08:00
import { EcommerceProgressBar } from "./EcommerceProgressBar" ;
2026-06-02 14:34:55 +08:00
const OSS_MUBAN = "https://stringtest.oss-cn-hangzhou.aliyuncs.com/muban" ;
const ecommerceGenerated = ` ${ OSS_MUBAN } /ecommerce-carousel-generated.png ` ;
const ecommerceSlide4 = ` ${ OSS_MUBAN } /slide-4.png ` ;
const ecommerceSlide5 = ` ${ OSS_MUBAN } /slide-5.png ` ;
2026-06-02 12:38:01 +08:00
import ImageMentionMenu , { getImageMentionQuery , insertImageMentionValue , type MentionImageOption } from "./ImageMentionMenu" ;
import EcommerceVideoWorkspace from "./EcommerceVideoWorkspace" ;
2026-06-03 20:19:07 +08:00
import EcommerceDetailPanel from "./panels/EcommerceDetailPanel" ;
import EcommerceSetPanel from "./panels/EcommerceSetPanel" ;
import EcommerceTryOnPanel from "./panels/EcommerceTryOnPanel" ;
import EcommerceClonePanel from "./panels/EcommerceClonePanel" ;
2026-06-02 12:38:01 +08:00
import { aiGenerationClient } from "../../api/aiGenerationClient" ;
2026-06-02 21:19:52 +08:00
import { ServerRequestError } from "../../api/serverConnection" ;
import { waitForTask } from "../../api/taskSubscription" ;
2026-06-03 20:19:07 +08:00
import { toast } from "../../components/toast/toastStore" ;
import { useGenerationTasks } from "../../hooks/useGenerationTasks" ;
import { useAppStore } from "../../stores" ;
2026-06-02 12:38:01 +08:00
import {
2026-06-03 20:19:07 +08:00
normalizeEcommerceImageMime ,
summarizeRejectedImages ,
validateEcommerceImageFiles ,
} from "./ecommerceImageValidation" ;
2026-06-02 12:38:01 +08:00
interface ProductClonePageProps {
[ key : string ] : unknown ;
}
2026-06-03 20:19:07 +08:00
type ProductCloneStatus = "idle" | "ready" | "generating" | "done" | "failed" ;
2026-06-02 12:38:01 +08:00
type ProductSetOutputKey = "set" | "detail" | "model" | "video" ;
2026-06-03 20:19:07 +08:00
type CloneOutputKey = ProductSetOutputKey | "hot" | "video-outfit" ;
2026-06-02 12:38:01 +08:00
type CloneSetCountKey = "selling" | "white" | "scene" ;
type CloneModelPanelTab = "scene" | "model" ;
type CloneVideoQualityKey = "standard" | "high" | "ultra" ;
2026-06-03 20:19:07 +08:00
type ProductSetStatus = "idle" | "ready" | "generating" | "done" | "failed" ;
2026-06-02 12:38:01 +08:00
type ProductKitToolKey = "set" | "detail" | "wear" | "clone" ;
type CloneBasicSelectKey = "platform" | "market" | "language" | "ratio" ;
type CloneModelSelectKey = "gender" | "age" | "ethnicity" | "body" ;
type CloneReferenceMode = "upload" | "link" ;
type CloneReplicateLevelKey = "style" | "high" ;
type TryOnModelSource = "ai" | "library" ;
2026-06-03 20:19:07 +08:00
type TryOnStatus = "idle" | "modeling" | "ready" | "generating" | "done" | "failed" ;
type DetailStatus = "idle" | "ready" | "generating" | "done" | "failed" ;
2026-06-02 12:38:01 +08:00
interface CloneImageItem {
id : string ;
src : string ;
name : string ;
width? : number ;
height? : number ;
format? : string ;
}
interface CloneResult {
id : string ;
src : string ;
label : string ;
}
interface CloneSavedSetting {
id : string ;
name : string ;
savedAt : string ;
output : CloneOutputKey ;
platform : string ;
market : string ;
language : string ;
ratio : string ;
setCounts : Record < CloneSetCountKey , number > ;
detailModules : string [ ] ;
modelPanelTab : CloneModelPanelTab ;
modelScenes : string [ ] ;
modelCustomScene : string ;
modelGender : string ;
modelAge : string ;
modelEthnicity : string ;
modelBody : string ;
modelAppearance : string ;
videoQuality : CloneVideoQualityKey ;
videoDurationSeconds : number ;
videoSmart : boolean ;
referenceMode? : CloneReferenceMode ;
replicateLevel? : CloneReplicateLevelKey ;
requirement : string ;
}
2026-06-03 20:19:07 +08:00
type PlatformRatioModeKey = ProductSetOutputKey | "hot" | "video-outfit" ;
2026-06-02 12:38:01 +08:00
interface PlatformRatioGroup {
ratios : string [ ] ;
defaultRatio : string ;
}
const sideTools : Array < { key : ProductKitToolKey ; label : string ; icon : ReactNode } > = [
{ key : "set" , label : "商品套图" , icon : < AppstoreOutlined / > } ,
{ key : "detail" , label : "A+详情" , icon : < FileImageOutlined / > } ,
{ key : "wear" , label : "服饰穿戴" , icon : < SkinOutlined / > } ,
{ key : "clone" , label : "电商AI作图" , icon : < AppstoreOutlined / > } ,
] ;
const platformSpecOptions : Array < {
label : string ;
ratios : string [ ] ;
defaultRatio : string ;
ratioGroups? : Partial < Record < PlatformRatioModeKey , PlatformRatioGroup > > ;
specs : string [ ] ;
tip? : string ;
aliases? : string [ ] ;
} > = [
{
label : "淘宝/天猫" ,
ratios : [ "淘宝主图 / SKU 图 800× 800px" , "详情页宽 750px" , "详情页宽 790px" ] ,
defaultRatio : "淘宝主图 / SKU 图 800× 800px" ,
ratioGroups : {
set : {
ratios : [ "1000× 1000px\u00a0\u00a0\u00a01: 1" , "800× 800px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "1000× 1000px\u00a0\u00a0\u00a01: 1" ,
} ,
detail : {
ratios : [
"750× 1000px\u00a0\u00a0\u00a03: 4" ,
"790× 1053px\u00a0\u00a0\u00a03: 4" ,
"750× 1125px\u00a0\u00a0\u00a02: 3" ,
"790× 1185px\u00a0\u00a0\u00a02: 3" ,
] ,
defaultRatio : "750× 1000px\u00a0\u00a0\u00a03: 4" ,
} ,
model : {
ratios : [ "750× 1000px\u00a0\u00a0\u00a03: 4" ] ,
defaultRatio : "750× 1000px\u00a0\u00a0\u00a03: 4" ,
} ,
video : {
ratios : [ "1080× 1920px\u00a0\u00a0\u00a09: 16" , "1080× 1440px\u00a0\u00a0\u00a03: 4" , "1080× 1080px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "1080× 1920px\u00a0\u00a0\u00a09: 16" ,
} ,
hot : {
ratios : [ "800× 800px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "800× 800px\u00a0\u00a0\u00a01: 1" ,
} ,
} ,
specs : [ "主图 / SKU 图 800× 800px,≤3MB" , "详情页宽 750px 或 790px,单张高≤1546px" ] ,
tip : "建议主图 200-400KB JPG,超过 500KB 会影响加载速度。" ,
} ,
{
label : "京东" ,
ratios : [ "京东主图 / SKU 图 800× 800px" , "详情页宽 750px" , "首图主体占比 ≥70%" ] ,
defaultRatio : "京东主图 / SKU 图 800× 800px" ,
ratioGroups : {
set : {
ratios : [ "1000× 1000px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "1000× 1000px\u00a0\u00a0\u00a01: 1" ,
} ,
detail : {
ratios : [
"750× 1000px\u00a0\u00a0\u00a03: 4" ,
"990× 1320px\u00a0\u00a0\u00a03: 4" ,
"750× 1125px\u00a0\u00a0\u00a02: 3" ,
"990× 1485px\u00a0\u00a0\u00a02: 3" ,
] ,
defaultRatio : "750× 1000px\u00a0\u00a0\u00a03: 4" ,
} ,
model : {
ratios : [ "750× 1125px\u00a0\u00a0\u00a02: 3" , "990× 1485px\u00a0\u00a0\u00a02: 3" ] ,
defaultRatio : "750× 1125px\u00a0\u00a0\u00a02: 3" ,
} ,
video : {
ratios : [ "1080× 1920px\u00a0\u00a0\u00a09: 16" , "1920× 1080px\u00a0\u00a0\u00a016: 9" ] ,
defaultRatio : "1080× 1920px\u00a0\u00a0\u00a09: 16" ,
} ,
hot : {
ratios : [ "800× 800px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "800× 800px\u00a0\u00a0\u00a01: 1" ,
} ,
} ,
specs : [ "主图 / SKU 图 800× 800px,白底,≤1MB" , "详情页宽 750px,首图主体占比 ≥70%" ] ,
} ,
{
label : "拼多多" ,
ratios : [ "主图 750× 352px" , "主图 800× 800px" , "详情页宽 750px" ] ,
defaultRatio : "主图 750× 352px" ,
ratioGroups : {
set : {
ratios : [ "800× 800px\u00a0\u00a0\u00a01: 1" , "750× 1000px\u00a0\u00a0\u00a03: 4" ] ,
defaultRatio : "800× 800px\u00a0\u00a0\u00a01: 1" ,
} ,
detail : {
ratios : [ "750× 1000px\u00a0\u00a0\u00a03: 4" , "750× 1125px\u00a0\u00a0\u00a02: 3" ] ,
defaultRatio : "750× 1000px\u00a0\u00a0\u00a03: 4" ,
} ,
model : {
ratios : [ "750× 1000px\u00a0\u00a0\u00a03: 4" ] ,
defaultRatio : "750× 1000px\u00a0\u00a0\u00a03: 4" ,
} ,
video : {
ratios : [ "1080× 1920px\u00a0\u00a0\u00a09: 16" ] ,
defaultRatio : "1080× 1920px\u00a0\u00a0\u00a09: 16" ,
} ,
hot : {
ratios : [ "800× 800px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "800× 800px\u00a0\u00a0\u00a01: 1" ,
} ,
} ,
specs : [ "主图 750× 352px 或 800× 800px,≤1MB" , "详情页宽 750px,要求纯白底、无水印、无拼接" ] ,
} ,
{
label : "抖音电商" ,
ratios : [ "短视频 1080× 1920px" ] ,
defaultRatio : "短视频 1080× 1920px" ,
ratioGroups : {
video : {
ratios : [ "1080× 1920px\u00a0\u00a0\u00a09: 16" ] ,
defaultRatio : "1080× 1920px\u00a0\u00a0\u00a09: 16" ,
} ,
hot : {
ratios : [ "800× 800px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "800× 800px\u00a0\u00a0\u00a01: 1" ,
} ,
} ,
specs : [ "短视频 1080× 1920px, 9:16" , "30s 内最佳" ] ,
} ,
{
label : "亚马逊 Amazon" ,
ratios : [ "主图 ≥1600× 1600px" , "建议 2000× 2000px+" , "最小 500× 500px" ] ,
defaultRatio : "主图 ≥1600× 1600px" ,
ratioGroups : {
set : {
ratios : [ "1600× 1600px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "1600× 1600px\u00a0\u00a0\u00a01: 1" ,
} ,
detail : {
ratios : [ "1600× 1600px\u00a0\u00a0\u00a01: 1" , "1200× 1800px\u00a0\u00a0\u00a02: 3" , "1200× 1600px\u00a0\u00a0\u00a03: 4" ] ,
defaultRatio : "1200× 1800px\u00a0\u00a0\u00a02: 3" ,
} ,
model : {
ratios : [ "1200× 1800px\u00a0\u00a0\u00a02: 3" ] ,
defaultRatio : "1200× 1800px\u00a0\u00a0\u00a02: 3" ,
} ,
video : {
ratios : [ "1920× 1080px\u00a0\u00a0\u00a016: 9" ] ,
defaultRatio : "1920× 1080px\u00a0\u00a0\u00a016: 9" ,
} ,
hot : {
ratios : [ "1600× 1600px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "1600× 1600px\u00a0\u00a0\u00a01: 1" ,
} ,
} ,
specs : [ "主图 1600× 1600px+,纯白底,≤10MB" , "最小 500× 500px,建议 2000px+ 以支持缩放" ] ,
aliases : [ "亚马逊" ] ,
} ,
{
label : "Shopee" ,
ratios : [ "商品主图 1024× 1024px" , "基础主图 800× 800px" ] ,
defaultRatio : "商品主图 1024× 1024px" ,
ratioGroups : {
set : {
ratios : [ "800× 800px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "800× 800px\u00a0\u00a0\u00a01: 1" ,
} ,
detail : {
ratios : [ "750× 1000px\u00a0\u00a0\u00a03: 4" , "750× 1125px\u00a0\u00a0\u00a02: 3" ] ,
defaultRatio : "750× 1000px\u00a0\u00a0\u00a03: 4" ,
} ,
model : {
ratios : [ "750× 1000px\u00a0\u00a0\u00a03: 4" ] ,
defaultRatio : "750× 1000px\u00a0\u00a0\u00a03: 4" ,
} ,
video : {
ratios : [ "1080× 1920px\u00a0\u00a0\u00a09: 16" ] ,
defaultRatio : "1080× 1920px\u00a0\u00a0\u00a09: 16" ,
} ,
hot : {
ratios : [ "800× 800px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "800× 800px\u00a0\u00a0\u00a01: 1" ,
} ,
} ,
specs : [ "商品主图推荐 1024× 1024px,基础 800× 800px" , "≤2MB,白底或浅色底" ] ,
aliases : [ "虾皮 Shopee/Lazada" , "虾皮" ] ,
} ,
{
label : "Lazada" ,
ratios : [ "商品主图 800× 800px" ] ,
defaultRatio : "商品主图 800× 800px" ,
ratioGroups : {
set : {
ratios : [ "800× 800px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "800× 800px\u00a0\u00a0\u00a01: 1" ,
} ,
detail : {
ratios : [ "750× 1000px\u00a0\u00a0\u00a03: 4" , "750× 1125px\u00a0\u00a0\u00a02: 3" ] ,
defaultRatio : "750× 1000px\u00a0\u00a0\u00a03: 4" ,
} ,
model : {
ratios : [ "750× 1000px\u00a0\u00a0\u00a03: 4" ] ,
defaultRatio : "750× 1000px\u00a0\u00a0\u00a03: 4" ,
} ,
video : {
ratios : [ "1080× 1920px\u00a0\u00a0\u00a09: 16" ] ,
defaultRatio : "1080× 1920px\u00a0\u00a0\u00a09: 16" ,
} ,
hot : {
ratios : [ "800× 800px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "800× 800px\u00a0\u00a0\u00a01: 1" ,
} ,
} ,
specs : [ "商品主图 800× 800px, 1:1" ] ,
} ,
{
label : "Instagram" ,
ratios : [ "帖子 1080× 1350px" , "帖子 1080× 1080px" , "Stories / Reels 1080× 1920px" , "头像 320× 320px" ] ,
defaultRatio : "帖子 1080× 1350px" ,
ratioGroups : {
set : {
ratios : [ "1080× 1080px\u00a0\u00a0\u00a01: 1" , "1080× 1350px\u00a0\u00a0\u00a04: 5" ] ,
defaultRatio : "1080× 1080px\u00a0\u00a0\u00a01: 1" ,
} ,
detail : {
ratios : [ "1080× 1350px\u00a0\u00a0\u00a04: 5" ] ,
defaultRatio : "1080× 1350px\u00a0\u00a0\u00a04: 5" ,
} ,
model : {
ratios : [ "1080× 1350px\u00a0\u00a0\u00a04: 5" ] ,
defaultRatio : "1080× 1350px\u00a0\u00a0\u00a04: 5" ,
} ,
video : {
ratios : [ "1080× 1920px\u00a0\u00a0\u00a09: 16" , "1080× 1350px\u00a0\u00a0\u00a04: 5" ] ,
defaultRatio : "1080× 1920px\u00a0\u00a0\u00a09: 16" ,
} ,
} ,
specs : [ "帖子 1080× 1350px 或 1080× 1080px" , "Stories / Reels 封面 1080× 1920px,头像 320× 320px" ] ,
tip : "建议 ≤1MB JPG。" ,
aliases : [ "Instagram Reels" ] ,
} ,
{
label : "速卖通" ,
ratios : [ "主图 800× 800px" , "主图 1000× 1000px+" ] ,
defaultRatio : "主图 800× 800px" ,
ratioGroups : {
set : {
ratios : [ "1000× 1000px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "1000× 1000px\u00a0\u00a0\u00a01: 1" ,
} ,
detail : {
ratios : [ "750× 1125px\u00a0\u00a0\u00a02: 3" , "750× 1000px\u00a0\u00a0\u00a03: 4" ] ,
defaultRatio : "750× 1125px\u00a0\u00a0\u00a02: 3" ,
} ,
model : {
ratios : [ "750× 1125px\u00a0\u00a0\u00a02: 3" ] ,
defaultRatio : "750× 1125px\u00a0\u00a0\u00a02: 3" ,
} ,
video : {
ratios : [ "1080× 1920px\u00a0\u00a0\u00a09: 16" , "1920× 1080px\u00a0\u00a0\u00a016: 9" ] ,
defaultRatio : "1080× 1920px\u00a0\u00a0\u00a09: 16" ,
} ,
hot : {
ratios : [ "800× 800px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "800× 800px\u00a0\u00a0\u00a01: 1" ,
} ,
} ,
specs : [ "主图建议 800× 800px 或更高,1:1" , "适合跨境电商主图、SKU 图和场景图" ] ,
} ,
{
label : "eBay" ,
ratios : [ "商品图 1:1" , "白底多角度展示图 1:1" ] ,
defaultRatio : "商品图 1:1" ,
ratioGroups : {
set : {
ratios : [ "1600× 1600px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "1600× 1600px\u00a0\u00a0\u00a01: 1" ,
} ,
detail : {
ratios : [ "1000× 1500px\u00a0\u00a0\u00a02: 3" , "1200× 1600px\u00a0\u00a0\u00a03: 4" ] ,
defaultRatio : "1000× 1500px\u00a0\u00a0\u00a02: 3" ,
} ,
model : {
ratios : [ "1000× 1500px\u00a0\u00a0\u00a02: 3" ] ,
defaultRatio : "1000× 1500px\u00a0\u00a0\u00a02: 3" ,
} ,
video : {
ratios : [ "1920× 1080px\u00a0\u00a0\u00a016: 9" , "1080× 1920px\u00a0\u00a0\u00a09: 16" ] ,
defaultRatio : "1920× 1080px\u00a0\u00a0\u00a016: 9" ,
} ,
hot : {
ratios : [ "1600× 1600px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "1600× 1600px\u00a0\u00a0\u00a01: 1" ,
} ,
} ,
specs : [ "商品图建议 1:1,主体清晰居中" , "适合白底主图和多角度展示图" ] ,
} ,
{
label : "TikTok Shop" ,
ratios : [ "商品主图 1:1" , "短视频 / 竖版封面 9:16" ] ,
defaultRatio : "商品主图 1:1" ,
ratioGroups : {
set : {
ratios : [ "1280× 1280px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "1280× 1280px\u00a0\u00a0\u00a01: 1" ,
} ,
detail : {
ratios : [ "1080× 1350px\u00a0\u00a0\u00a04: 5" ] ,
defaultRatio : "1080× 1350px\u00a0\u00a0\u00a04: 5" ,
} ,
model : {
ratios : [ "1080× 1350px\u00a0\u00a0\u00a04: 5" ] ,
defaultRatio : "1080× 1350px\u00a0\u00a0\u00a04: 5" ,
} ,
video : {
ratios : [ "1080× 1920px\u00a0\u00a0\u00a09: 16" ] ,
defaultRatio : "1080× 1920px\u00a0\u00a0\u00a09: 16" ,
} ,
hot : {
ratios : [ "800× 800px\u00a0\u00a0\u00a01: 1" ] ,
defaultRatio : "800× 800px\u00a0\u00a0\u00a01: 1" ,
} ,
} ,
specs : [ "商品主图建议 1:1" , "短视频/竖版封面建议 9:16" ] ,
} ,
] ;
const platformOptions = platformSpecOptions . map ( ( option ) = > option . label ) ;
const marketLanguageOptions : Array < { country : string ; languages : string [ ] } > = [
{ country : "中国" , languages : [ "中文" ] } ,
{ country : "美国" , languages : [ "英文" ] } ,
{ country : "加拿大" , languages : [ "英文" , "法文" ] } ,
{ country : "英国" , languages : [ "英文" ] } ,
{ country : "德国" , languages : [ "德文" ] } ,
{ country : "法国" , languages : [ "法文" ] } ,
{ country : "意大利" , languages : [ "意大利语" ] } ,
{ country : "西班牙" , languages : [ "西班牙语" ] } ,
{ country : "日本" , languages : [ "日文" ] } ,
{ country : "韩国" , languages : [ "韩文" ] } ,
{ country : "澳大利亚" , languages : [ "英文" ] } ,
{ country : "新加坡" , languages : [ "英文" , "中文" ] } ,
{ country : "马来西亚" , languages : [ "马来语" , "英文" , "中文" ] } ,
{ country : "印尼" , languages : [ "印度尼西亚语" , "英文" ] } ,
{ country : "越南" , languages : [ "越南语" , "英文" ] } ,
{ country : "泰国" , languages : [ "泰语" , "英文" ] } ,
{ country : "菲律宾" , languages : [ "菲律宾语(他加禄语)" , "英文" ] } ,
{ country : "巴西" , languages : [ "葡萄牙语" ] } ,
{ country : "墨西哥" , languages : [ "西班牙语" ] } ,
{ country : "智利" , languages : [ "西班牙语" ] } ,
{ country : "哥伦比亚" , languages : [ "西班牙语" ] } ,
{ country : "阿联酋" , languages : [ "阿拉伯语" , "英文" ] } ,
{ country : "沙特阿拉伯" , languages : [ "阿拉伯语" , "英文" ] } ,
{ country : "俄罗斯" , languages : [ "俄语" ] } ,
{ country : "波兰" , languages : [ "波兰语" ] } ,
] ;
const marketOptions = marketLanguageOptions . map ( ( option ) = > option . country ) ;
const languageOptions = Array . from ( new Set ( marketLanguageOptions . flatMap ( ( option ) = > option . languages ) ) ) ;
const languageAliases : Record < string , string > = {
英 语 : "英文" ,
日 语 : "日文" ,
德 语 : "德文" ,
法 语 : "法文" ,
韩 语 : "韩文" ,
西 文 : "西班牙语" ,
葡 文 : "葡萄牙语" ,
印 尼 语 : "印度尼西亚语" ,
菲 律 宾 语 : "菲律宾语(他加禄语)" ,
} ;
const defaultPlatformSpec = platformSpecOptions [ 0 ] ! ;
const getPlatformSpec = ( value : string ) = >
platformSpecOptions . find ( ( option ) = > option . label === value || option . aliases ? . includes ( value ) ) ? ? defaultPlatformSpec ;
const normalizePlatform = ( value : string ) = > getPlatformSpec ( value ) . label ;
const domesticPlatformLabels = new Set ( [ "淘宝/天猫" , "京东" , "拼多多" , "抖音电商" ] ) ;
const domesticPlatformLanguages = [ "中文" ] ;
const isDomesticPlatform = ( platformValue : string ) = > domesticPlatformLabels . has ( normalizePlatform ( platformValue ) ) ;
const getPlatformRatioGroup = ( value : string , mode? : PlatformRatioModeKey ) : PlatformRatioGroup = > {
const platformSpec = getPlatformSpec ( value ) ;
return ( mode ? platformSpec . ratioGroups ? . [ mode ] : null ) ? ? {
ratios : platformSpec.ratios ,
defaultRatio : platformSpec.defaultRatio ,
} ;
} ;
const getPlatformRatioOptions = ( value : string , mode? : PlatformRatioModeKey ) = > getPlatformRatioGroup ( value , mode ) . ratios ;
const getPlatformDefaultRatio = ( value : string , mode? : PlatformRatioModeKey ) = > getPlatformRatioGroup ( value , mode ) . defaultRatio ;
const getUniqueRatioOptions = ( ratios : string [ ] ) = > Array . from ( new Set ( ratios ) ) ;
const normalizeRatioToken = ( value : string ) = > value . replace ( /: /g , ":" ) . trim ( ) ;
const normalizeRatioForPlatform = ( platformValue : string , ratioValue : string , mode? : PlatformRatioModeKey ) = > {
const platformRatios = getPlatformRatioOptions ( platformValue , mode ) ;
if ( platformRatios . includes ( ratioValue ) ) return ratioValue ;
const normalizedRatio = normalizeRatioToken ( ratioValue ) ;
const matchedRatio = platformRatios . find ( ( option ) = > normalizeRatioToken ( option ) . includes ( normalizedRatio ) ) ;
return matchedRatio ? ? getPlatformDefaultRatio ( platformValue , mode ) ;
} ;
const formatRatioDisplayValue = ( value : string ) = > {
if ( ! value . includes ( "套图" ) ) return value ;
const size = value . match ( / \ d + \ s * × \ s * \ d + \ s * p x ? / u ) ? . [ 0 ] ? . r e p l a c e ( / \ s + / g , " " ) ? ? " " ;
const ratio = value . match ( / \ d + ( ? : \ . \ d + ) ? \ s * [ : : ] \ s * \ d + ( ? : \ . \ d + ) ? / u ) ? . [ 0 ] ? . r e p l a c e ( / \ s + / g , " " ) . r e p l a c e ( / : / g , " : " ) ? ? " " ;
return size && ratio ? ` ${ size } \ u00a0 \ u00a0 \ u00a0 ${ ratio } ` : value . replace ( /^套图[:: ]\s*/ , "" ) ;
} ;
const greatestCommonDivisor = ( left : number , right : number ) : number = > {
let a = Math . abs ( left ) ;
let b = Math . abs ( right ) ;
while ( b ) {
[ a , b ] = [ b , a % b ] ;
}
return a || 1 ;
} ;
const formatAspectRatio = ( width : number , height : number ) = > {
const divisor = greatestCommonDivisor ( width , height ) ;
return ` ${ Math . round ( width / divisor ) } : ${ Math . round ( height / divisor ) } ` ;
} ;
const formatUploadedImageRatio = ( image? : CloneImageItem ) = > {
if ( ! image ) return null ;
const format = image . format ? ` \ u00a0 \ u00a0 \ u00a0 ${ image . format } ` : "" ;
if ( ! image . width || ! image . height ) return ` 上传图片 \ u00a0 \ u00a0 \ u00a0原图比例 ${ format } ` ;
return ` 上传图片 ${ image . width } × ${ image . height } px \ u00a0 \ u00a0 \ u00a0 ${ formatAspectRatio ( image . width , image . height ) } ${ format } ` ;
} ;
const defaultMarketLanguageOption = marketLanguageOptions [ 0 ] ! ;
const normalizeMarket = ( value : string ) = >
marketLanguageOptions . some ( ( option ) = > option . country === value ) ? value : defaultMarketLanguageOption.country ;
const normalizeLanguage = ( value : string ) = > languageAliases [ value ] ? ? value ;
const uniqueLanguages = ( languages : string [ ] ) = > Array . from ( new Set ( languages ) ) ;
const appendEnglish = ( languages : string [ ] ) = > Array . from ( new Set ( [ . . . languages , "英文" ] ) ) ;
const getMarketLanguageOptions = ( marketValue : string ) = >
appendEnglish ( ( marketLanguageOptions . find ( ( option ) = > option . country === marketValue ) ? ? defaultMarketLanguageOption ) . languages ) ;
const getPlatformLanguageOptions = ( platformValue : string , marketValue : string ) = > {
const marketLanguages = getMarketLanguageOptions ( marketValue ) ;
if ( ! isDomesticPlatform ( platformValue ) ) return marketLanguages ;
const localLanguages = marketLanguages . filter ( ( item ) = > item !== "英文" ) ;
return uniqueLanguages ( [ . . . localLanguages , . . . domesticPlatformLanguages , "英文" ] ) ;
} ;
const getPlatformDefaultLanguage = ( platformValue : string , marketValue : string ) = >
isDomesticPlatform ( platformValue ) ? "中文" : ( getPlatformLanguageOptions ( platformValue , marketValue ) [ 0 ] ? ? languageOptions [ 0 ] ? ? "英文" ) ;
const normalizeLanguageForPlatform = ( platformValue : string , marketValue : string , languageValue : string ) = > {
const normalizedLanguage = normalizeLanguage ( languageValue ) ;
const platformLanguages = getPlatformLanguageOptions ( platformValue , marketValue ) ;
return platformLanguages . includes ( normalizedLanguage ) ? normalizedLanguage : getPlatformDefaultLanguage ( platformValue , marketValue ) ;
} ;
const productSetOutputOptions : Array < { key : ProductSetOutputKey ; label : string } > = [
{ key : "set" , label : "套图" } ,
{ key : "detail" , label : "详情图" } ,
{ key : "model" , label : "模特图" } ,
{ key : "video" , label : "短视频" } ,
] ;
const cloneOutputOptions : Array < { key : CloneOutputKey ; label : string } > = [
. . . productSetOutputOptions ,
{ key : "hot" , label : "爆款图复刻" } ,
2026-06-03 20:19:07 +08:00
{ key : "video-outfit" , label : "视频换装" } ,
2026-06-02 12:38:01 +08:00
] ;
const cloneSetCountOptions : Array < {
key : CloneSetCountKey ;
title : string ;
desc : string ;
} > = [
{ key : "selling" , title : "卖点图" , desc : "展示商品的核心卖点及细节特写" } ,
{ key : "white" , title : "白底图" , desc : "白底主图,多角度呈现商品细节" } ,
{ key : "scene" , title : "场景图" , desc : "展示商品的生活使用场景和人物搭配" } ,
] ;
const defaultCloneSetCounts : Record < CloneSetCountKey , number > = {
selling : 3 ,
white : 1 ,
scene : 3 ,
} ;
const minCloneSetTotal = 1 ;
const maxCloneSetTotal = 16 ;
const maxCloneProductImages = 7 ;
const maxCloneReferenceImages = 20 ;
const cloneVideoDurationMin = 5 ;
const cloneVideoDurationMax = 15 ;
const cloneLatestSettingStorageKey = "omniai.clone-ai.latest-setting" ;
const cloneVideoQualityOptions : Array < { key : CloneVideoQualityKey ; label : string ; desc : string } > = [
{ key : "standard" , label : "标准" , desc : "快速出片" } ,
{ key : "high" , label : "高清" , desc : "推荐" } ,
{ key : "ultra" , label : "超清" , desc : "细节增强" } ,
] ;
const cloneReplicateLevelOptions : Array < { key : CloneReplicateLevelKey ; title : string ; desc : string } > = [
{ key : "style" , title : "参考风格" , desc : "参考整体风格和结构,自动调整色彩和重构场景。" } ,
{ key : "high" , title : "高度复刻" , desc : "参照参考图视觉结构替换产品和文案,场景细节略有差异。" } ,
] ;
const tryOnRatioOptions = [ "3:4" , "1:1" , "9:16" ] ;
const tryOnScenes = [ "纯色棚拍" , "都市街头" , "街角咖啡" , "自然草坪" , "度假海滩" , "温馨居家" , "艺术展馆" ] ;
const normalizeCloneModelSceneSelection = ( scenes : string [ ] | null | undefined ) = > {
const validScenes = ( scenes ? ? [ ] ) . filter ( ( scene ) = > typeof scene === "string" && scene . trim ( ) ) ;
const latestScene = validScenes [ validScenes . length - 1 ] ;
return latestScene ? [ latestScene ] : [ ] ;
} ;
const tryOnModelOptions = {
gender : [ "女" , "男" ] ,
age : [ "青年" , "少年" , "中年" ] ,
ethnicity : [ "欧美白人" , "亚洲人" , "拉美裔" , "非洲裔" ] ,
body : [ "标准" , "高挑" , "微胖" , "运动" ] ,
} ;
const sampleResults = [ ecommerceSlide4 , ecommerceGenerated , ecommerceSlide5 ] ;
const productSetAssets = {
main : "https://xiuxiu-pro.meitudata.com/poster/6e3eebacad8d5e47e1896ee7d54827bc.png?imageView2/2/w/800/format/webp/q/80/ignore-error/1" ,
scene : "https://xiuxiu-pro.meitudata.com/poster/21225fc86b28d9e4d85636483c67408e.png?imageView2/2/w/400/format/webp/q/80/ignore-error/1" ,
model : "https://xiuxiu-pro.meitudata.com/poster/4b8e6d1bd0996be52822dd1fac73cffd.png?imageView2/2/w/400/format/webp/q/80/ignore-error/1" ,
detail : "https://xiuxiu-pro.meitudata.com/poster/29dd195a450ee5a7f7451ded6680e969.png?imageView2/2/w/400/format/webp/q/80/ignore-error/1" ,
selling : "https://xiuxiu-pro.meitudata.com/poster/66bdef541b67588e8db2a03b39dc815b.jpg?imageView2/2/w/400/format/webp/q/80/ignore-error/1" ,
hosting : "https://xiuxiu-pro-new.meitudata.com/poster/50c17a98c77fac4d0523c8cbdf0d33ca.jpg?imageView2/2/format/webp/q/80/ignore-error/1" ,
} ;
const productSetPreviewCards = [
{ id : "main" , label : "01 主图 (白底/合规)" , src : productSetAssets.main } ,
{ id : "scene" , label : "02 场景展示" , src : productSetAssets.scene } ,
{ id : "model" , label : "03 模特场景图" , src : productSetAssets.model } ,
{ id : "detail" , label : "04 细节说明" , src : productSetAssets.detail } ,
{ id : "selling" , label : "05 卖点详解" , src : productSetAssets.selling } ,
] ;
const tryOnAssets = {
dressA : "https://xiuxiu-pro-new.meitudata.com/poster/133ca2d6c13bac6cfaa11fa29a155551.jpg?imageView2/2/w/280/format/webp/q/80/ignore-error/1" ,
dressB : "https://xiuxiu-pro-new.meitudata.com/poster/a661006820e888d9df13023075096e94.jpg?imageView2/2/w/280/format/webp/q/80/ignore-error/1" ,
modelWoman : "https://xiuxiu-pro-new.meitudata.com/poster/f806c6afaf6f38f634c156c5b6058201.png?imageView2/2/w/280/format/webp/q/80/ignore-error/1" ,
modelMan : "https://xiuxiu-pro-new.meitudata.com/poster/8c26503c67dc695e25e420e48caf4cde.png?imageView2/2/w/280/format/webp/q/80/ignore-error/1" ,
modelAsian : "https://xiuxiu-pro-new.meitudata.com/poster/0f2a7c92707312ec74647d66f15a6ef9.jpg?imageView2/2/w/280/format/webp/q/80/ignore-error/1" ,
tryA : "https://xiuxiu-pro-new.meitudata.com/poster/7f77e0866f05ff723959e1f48830713c.jpg?imageView2/2/w/345/format/webp/q/80/ignore-error/1" ,
tryB : "https://xiuxiu-pro-new.meitudata.com/poster/0b951004eabcdd7cae595dfdb4c7f8c3.jpg?imageView2/2/w/345/format/webp/q/80/ignore-error/1" ,
jacket : "https://xiuxiu-pro-new.meitudata.com/poster/fdbf10b4c92af5b1986444cdd9affaa5.png?imageView2/2/w/280/format/webp/q/80/ignore-error/1" ,
jacketResultA : "https://xiuxiu-pro-new.meitudata.com/poster/b1152bb292323b87696dd2f6e518e818.jpg?imageView2/2/w/345/format/webp/q/80/ignore-error/1" ,
jacketResultB : "https://xiuxiu-pro-new.meitudata.com/poster/1c1e757702108fef92d85be0c2802c01.jpg?imageView2/2/w/345/format/webp/q/80/ignore-error/1" ,
hat : "https://xiuxiu-pro-new.meitudata.com/poster/278af735b076ab812888802d3e3db0b8.jpg?imageView2/2/w/280/format/webp/q/80/ignore-error/1" ,
hatResultA : "https://xiuxiu-pro-new.meitudata.com/poster/a3ba241b7aa6060869b096d3f10e5db4.jpg?imageView2/2/w/345/format/webp/q/80/ignore-error/1" ,
hatResultB : "https://xiuxiu-pro-new.meitudata.com/poster/01ed1ae80a187c70c682bb6d0ec6fa68.jpg?imageView2/2/w/345/format/webp/q/80/ignore-error/1" ,
} ;
const tryOnCards = [
{
title : "多件混搭自动融合" ,
tone : "red" ,
inputs : [ tryOnAssets . dressA , tryOnAssets . dressB , tryOnAssets . modelWoman ] ,
results : [ tryOnAssets . tryA , tryOnAssets . tryB ] ,
} ,
{
title : "一件也能出大片" ,
tone : "brown" ,
inputs : [ tryOnAssets . jacket , tryOnAssets . modelMan ] ,
results : [ tryOnAssets . jacketResultA , tryOnAssets . jacketResultB ] ,
} ,
{
title : "鞋帽饰品完美适配" ,
tone : "gold" ,
inputs : [ tryOnAssets . hat , tryOnAssets . modelAsian ] ,
results : [ tryOnAssets . hatResultA , tryOnAssets . hatResultB ] ,
} ,
] ;
const detailTypeOptions = [ "普通A+" , "品牌A+" , "标准详情页" , "移动端长图" ] ;
const detailModules = [
{ id : "hero" , title : "首屏主视觉" , desc : "传递核心价值" } ,
{ id : "selling" , title : "核心卖点图" , desc : "突出卖点优势" } ,
{ id : "usage" , title : "使用场景图" , desc : "呈现真实使用场景" } ,
{ id : "angle" , title : "多角度图" , desc : "多角度呈现外观" } ,
{ id : "scene" , title : "场景氛围图" , desc : "展示使用场景" } ,
{ id : "detail" , title : "商品细节图" , desc : "放大材质与工艺" } ,
{ id : "story" , title : "品牌故事图" , desc : "传达品牌理念" } ,
{ id : "size" , title : "尺寸/容量/尺码图" , desc : "展示规格信息" } ,
{ id : "compare" , title : "效果对比图" , desc : "使用前后效果对比" } ,
{ id : "spec" , title : "详细规格/参数表" , desc : "展示详细商品数据" } ,
{ id : "craft" , title : "工艺制作图" , desc : "展示工艺制作过程" } ,
{ id : "gift" , title : "配件/赠品图" , desc : "明确收货的所有物品" } ,
{ id : "series" , title : "系列展示图" , desc : "多色或多SKU展示" } ,
{ id : "ingredient" , title : "商品成分图" , desc : "展示配方/材质/成分" } ,
{ id : "service" , title : "售后保障图" , desc : "说明质保退换政策" } ,
{ id : "tips" , title : "使用建议图" , desc : "商品使用的注意事项" } ,
] ;
const defaultDetailModuleIds : string [ ] = [ ] ;
const defaultCloneDetailModuleIds = [ "hero" , "selling" , "usage" , "angle" , "scene" , "detail" ] ;
const cloneDetailModules = detailModules ;
const detailAssets = {
productA : "https://xiuxiu-pro.meitudata.com/poster/182676711565ee98e20cf92d766d1643.png?imageView2/2/format/webp/q/80/ignore-error/1" ,
productB : "https://xiuxiu-pro.meitudata.com/poster/ba6312cbc3a32ceb8966f9ea20b9ee9c.png?imageView2/2/format/webp/q/80/ignore-error/1" ,
productC : "https://xiuxiu-pro.meitudata.com/poster/7ee5753a3141fa12cda155126c8225d3.png?imageView2/2/format/webp/q/80/ignore-error/1" ,
longPage : "https://xiuxiu-pro.meitudata.com/poster/19ef313484fc87c9bdd3cd52ce2a5947.png?imageView2/2/format/webp/q/80/ignore-error/1" ,
gridA : "https://xiuxiu-pro.meitudata.com/poster/e74e8d920ac0f87020f90457d42a7153.png?imageView2/2/format/webp/q/80/ignore-error/1" ,
gridB : "https://xiuxiu-pro.meitudata.com/poster/1652064f17c5c2b32ce287244b505c15.png?imageView2/2/format/webp/q/80/ignore-error/1" ,
gridC : "https://xiuxiu-pro.meitudata.com/poster/dd8abace327edf61d8a8e2d7db42cfbe.png?imageView2/2/format/webp/q/80/ignore-error/1" ,
gridD : "https://xiuxiu-pro.meitudata.com/poster/7dc397f1cb76a35f7f0ed3c3ce78ba81.png?imageView2/2/format/webp/q/80/ignore-error/1" ,
gridE : "https://xiuxiu-pro.meitudata.com/poster/1199bd8b968a5162752e1ee2b093d315.png?imageView2/2/format/webp/q/80/ignore-error/1" ,
gridF : "https://xiuxiu-pro.meitudata.com/poster/7a8cdb3693418df9915741960f8f5aa8.png?imageView2/2/format/webp/q/80/ignore-error/1" ,
} ;
const detailProductSamples = [ detailAssets . productA , detailAssets . productB , detailAssets . productC ] ;
const detailGridSamples = [ detailAssets . gridA , detailAssets . gridB , detailAssets . gridC , detailAssets . gridD , detailAssets . gridE , detailAssets . gridF ] ;
function getImageFileFormat ( file : File ) {
const mimeFormat = file . type . split ( "/" ) [ 1 ] ? . replace ( "jpeg" , "jpg" ) . toUpperCase ( ) ;
if ( mimeFormat ) return mimeFormat ;
return file . name . split ( "." ) . pop ( ) ? . toUpperCase ( ) ? ? "" ;
}
function readImageDimensions ( src : string ) : Promise < { width : number ; height : number } > {
return new Promise ( ( resolve , reject ) = > {
const image = new Image ( ) ;
image . onload = ( ) = > resolve ( { width : image.naturalWidth , height : image.naturalHeight } ) ;
image . onerror = reject ;
image . src = src ;
} ) ;
}
function createObjectImageItems ( files : File [ ] , limit : number , prefix : string ) {
return Array . from ( files )
. slice ( 0 , limit )
. map < CloneImageItem > ( ( file , index ) = > ( {
id : ` ${ prefix } - ${ Date . now ( ) } - ${ index } ` ,
src : URL.createObjectURL ( file ) ,
name : file.name ,
format : getImageFileFormat ( file ) ,
} ) ) ;
}
2026-06-03 20:19:07 +08:00
function notifyRejectedImages ( files : File [ ] ) : File [ ] {
const { accepted , rejected } = validateEcommerceImageFiles ( files ) ;
const message = summarizeRejectedImages ( rejected ) ;
if ( message ) toast . error ( message ) ;
return accepted ;
}
2026-06-02 12:38:01 +08:00
function isCloneSavedSetting ( item : unknown ) : item is CloneSavedSetting {
const candidate = item as Partial < CloneSavedSetting > ;
return (
typeof candidate . id === "string" &&
typeof candidate . name === "string" &&
typeof candidate . savedAt === "string" &&
typeof candidate . output === "string" &&
typeof candidate . platform === "string" &&
typeof candidate . market === "string" &&
typeof candidate . language === "string" &&
typeof candidate . ratio === "string" &&
typeof candidate . videoDurationSeconds === "number"
) ;
}
function readCloneLatestSetting() {
if ( typeof window === "undefined" ) return null ;
try {
const rawValue = window . localStorage . getItem ( cloneLatestSettingStorageKey ) ;
if ( rawValue ) {
const parsedValue : unknown = JSON . parse ( rawValue ) ;
if ( isCloneSavedSetting ( parsedValue ) ) return parsedValue ;
}
} catch {
return null ;
}
return null ;
}
function writeCloneLatestSetting ( setting : CloneSavedSetting ) {
if ( typeof window === "undefined" ) return ;
window . localStorage . setItem ( cloneLatestSettingStorageKey , JSON . stringify ( setting ) ) ;
}
function clampCloneVideoDuration ( value : number ) {
return Math . min ( cloneVideoDurationMax , Math . max ( cloneVideoDurationMin , Math . round ( value ) ) ) ;
}
function ProductClonePage ( _props : ProductClonePageProps = { } ) {
const setInputRef = useRef < HTMLInputElement > ( null ) ;
const productInputRef = useRef < HTMLInputElement > ( null ) ;
const cloneReferenceInputRef = useRef < HTMLInputElement > ( null ) ;
const requirementTextareaRef = useRef < HTMLTextAreaElement > ( null ) ;
const garmentInputRef = useRef < HTMLInputElement > ( null ) ;
const detailInputRef = useRef < HTMLInputElement > ( null ) ;
const countHoldTimeoutRef = useRef < number | null > ( null ) ;
const countHoldIntervalRef = useRef < number | null > ( null ) ;
2026-06-03 20:19:07 +08:00
const imageGen = useGenerationTasks ( { sourceView : "ecommerce" } ) ;
const appUsage = useAppStore ( ( s ) = > s . usage ) ;
2026-06-02 12:38:01 +08:00
const latestCloneSettingRef = useRef < CloneSavedSetting | null > ( null ) ;
const skipInitialCloneAutoSaveRef = useRef ( true ) ;
const skipNextCloneAutoSaveRef = useRef ( false ) ;
const [ activeTool , setActiveTool ] = useState < ProductKitToolKey > ( "clone" ) ;
const [ setImages , setSetImages ] = useState < CloneImageItem [ ] > ( [ ] ) ;
const [ productSetPlatform , setProductSetPlatform ] = useState ( platformOptions [ 0 ] ) ;
const [ productSetMarket , setProductSetMarket ] = useState ( marketOptions [ 0 ] ) ;
const [ productSetLanguage , setProductSetLanguage ] = useState ( getPlatformDefaultLanguage ( platformOptions [ 0 ] , marketOptions [ 0 ] ) ) ;
const [ productSetRatio , setProductSetRatio ] = useState ( getPlatformDefaultRatio ( platformOptions [ 0 ] ) ) ;
const [ productSetRequirement , setProductSetRequirement ] = useState ( "" ) ;
const [ productSetOutput , setProductSetOutput ] = useState < ProductSetOutputKey > ( "video" ) ;
const [ productSetStatus , setProductSetStatus ] = useState < ProductSetStatus > ( "idle" ) ;
2026-06-02 21:31:43 +08:00
const [ productSetResultImages , setProductSetResultImages ] = useState < string [ ] > ( [ ] ) ;
2026-06-02 12:38:01 +08:00
const [ isSetUploadDragging , setIsSetUploadDragging ] = useState ( false ) ;
const [ selectedProductSetPreview , setSelectedProductSetPreview ] = useState < { src : string ; label : string } | null > ( null ) ;
const [ showHostingModal , setShowHostingModal ] = useState ( false ) ;
const [ productImages , setProductImages ] = useState < CloneImageItem [ ] > ( [ ] ) ;
const [ isProductUploadDragging , setIsProductUploadDragging ] = useState ( false ) ;
const [ cloneOutput , setCloneOutput ] = useState < CloneOutputKey > ( "detail" ) ;
const [ openCloneBasicSelect , setOpenCloneBasicSelect ] = useState < CloneBasicSelectKey | null > ( null ) ;
const [ openCloneModelSelect , setOpenCloneModelSelect ] = useState < CloneModelSelectKey | null > ( null ) ;
const [ cloneModelSelectDropUp , setCloneModelSelectDropUp ] = useState ( false ) ;
const [ cloneReferenceMode , setCloneReferenceMode ] = useState < CloneReferenceMode > ( "upload" ) ;
const [ cloneReferenceImages , setCloneReferenceImages ] = useState < CloneImageItem [ ] > ( [ ] ) ;
const [ cloneReplicateLevel , setCloneReplicateLevel ] = useState < CloneReplicateLevelKey > ( "high" ) ;
const [ cloneSetCounts , setCloneSetCounts ] = useState ( defaultCloneSetCounts ) ;
const [ selectedCloneDetailModules , setSelectedCloneDetailModules ] = useState < string [ ] > ( defaultCloneDetailModuleIds ) ;
const [ cloneModelPanelTab , setCloneModelPanelTab ] = useState < CloneModelPanelTab > ( "scene" ) ;
const [ selectedCloneModelScenes , setSelectedCloneModelScenes ] = useState < string [ ] > ( [ ] ) ;
const [ cloneModelCustomScene , setCloneModelCustomScene ] = useState ( "" ) ;
const [ cloneModelGender , setCloneModelGender ] = useState ( tryOnModelOptions . gender [ 0 ] ) ;
const [ cloneModelAge , setCloneModelAge ] = useState ( tryOnModelOptions . age [ 0 ] ) ;
const [ cloneModelEthnicity , setCloneModelEthnicity ] = useState ( tryOnModelOptions . ethnicity [ 0 ] ) ;
const [ cloneModelBody , setCloneModelBody ] = useState ( tryOnModelOptions . body [ 0 ] ) ;
const [ cloneModelAppearance , setCloneModelAppearance ] = useState ( "" ) ;
const [ cloneVideoQuality , setCloneVideoQuality ] = useState < CloneVideoQualityKey > ( "high" ) ;
const [ cloneVideoDuration , setCloneVideoDuration ] = useState ( 10 ) ;
const [ cloneVideoSmart , setCloneVideoSmart ] = useState ( true ) ;
2026-06-03 20:19:07 +08:00
const [ videoOutfitVideoFile , setVideoOutfitVideoFile ] = useState < File | null > ( null ) ;
const [ videoOutfitRefFile , setVideoOutfitRefFile ] = useState < File | null > ( null ) ;
2026-06-02 12:38:01 +08:00
const [ isCloneSettingsCollapsed , setIsCloneSettingsCollapsed ] = useState ( false ) ;
const [ requirement , setRequirement ] = useState ( "" ) ;
const [ requirementImageMentionQuery , setRequirementImageMentionQuery ] = useState < string | null > ( null ) ;
const [ cloneSettingName , setCloneSettingName ] = useState ( "新建创作" ) ;
const [ platform , setPlatform ] = useState ( platformOptions [ 0 ] ) ;
const [ market , setMarket ] = useState ( marketOptions [ 0 ] ) ;
const [ language , setLanguage ] = useState ( getPlatformDefaultLanguage ( platformOptions [ 0 ] , marketOptions [ 0 ] ) ) ;
const [ ratio , setRatio ] = useState ( getPlatformDefaultRatio ( platformOptions [ 0 ] ) ) ;
const [ status , setStatus ] = useState < ProductCloneStatus > ( "idle" ) ;
const [ results , setResults ] = useState < CloneResult [ ] > ( [ ] ) ;
2026-06-02 21:19:52 +08:00
const imageAbortRef = useRef ( { current : false } ) ;
2026-06-03 20:19:07 +08:00
const lastFailedActionRef = useRef < ( ( ) = > void ) | null > ( null ) ;
2026-06-02 12:38:01 +08:00
const [ garmentImages , setGarmentImages ] = useState < CloneImageItem [ ] > ( [ ] ) ;
const [ modelSource , setModelSource ] = useState < TryOnModelSource > ( "ai" ) ;
const [ modelGender , setModelGender ] = useState ( tryOnModelOptions . gender [ 0 ] ) ;
const [ modelAge , setModelAge ] = useState ( tryOnModelOptions . age [ 0 ] ) ;
const [ modelEthnicity , setModelEthnicity ] = useState ( tryOnModelOptions . ethnicity [ 0 ] ) ;
const [ modelBody , setModelBody ] = useState ( tryOnModelOptions . body [ 0 ] ) ;
const [ appearance , setAppearance ] = useState ( "" ) ;
const [ selectedScenes , setSelectedScenes ] = useState < string [ ] > ( [ ] ) ;
const [ customScene , setCustomScene ] = useState ( "" ) ;
const [ smartScene , setSmartScene ] = useState ( false ) ;
const [ tryOnRatio , setTryOnRatio ] = useState ( tryOnRatioOptions [ 0 ] ) ;
const [ tryOnStatus , setTryOnStatus ] = useState < TryOnStatus > ( "idle" ) ;
const [ tryOnResultImages , setTryOnResultImages ] = useState < string [ ] > ( [ ] ) ;
const [ detailProductImages , setDetailProductImages ] = useState < CloneImageItem [ ] > ( [ ] ) ;
const [ detailPlatform , setDetailPlatform ] = useState ( platformOptions [ 0 ] ) ;
const [ detailMarket , setDetailMarket ] = useState ( marketOptions [ 0 ] ) ;
const [ detailLanguage , setDetailLanguage ] = useState ( getPlatformDefaultLanguage ( platformOptions [ 0 ] , marketOptions [ 0 ] ) ) ;
const [ detailType , setDetailType ] = useState ( detailTypeOptions [ 0 ] ) ;
const [ detailRequirement , setDetailRequirement ] = useState ( "" ) ;
const [ selectedDetailModules , setSelectedDetailModules ] = useState < string [ ] > ( defaultDetailModuleIds ) ;
const [ detailStatus , setDetailStatus ] = useState < DetailStatus > ( "idle" ) ;
2026-06-02 21:31:43 +08:00
const [ detailResultUrl , setDetailResultUrl ] = useState < string | null > ( null ) ;
2026-06-02 12:38:01 +08:00
const productSetRatioOptions = getPlatformRatioOptions ( productSetPlatform , productSetOutput ) ;
const hotUploadedRatioOption = cloneOutput === "hot" ? formatUploadedImageRatio ( cloneReferenceImages [ 0 ] ) : null ;
const baseCloneRatioOptions = getPlatformRatioOptions ( platform , cloneOutput ) ;
const cloneRatioOptions = hotUploadedRatioOption
? getUniqueRatioOptions ( [ . . . baseCloneRatioOptions , hotUploadedRatioOption ] )
: baseCloneRatioOptions ;
const productSetLanguageOptions = getPlatformLanguageOptions ( productSetPlatform , productSetMarket ) ;
const cloneLanguageOptions = getPlatformLanguageOptions ( platform , market ) ;
const detailLanguageOptions = getPlatformLanguageOptions ( detailPlatform , detailMarket ) ;
const ecommerceMentionImages : MentionImageOption [ ] = [
. . . productImages . map ( ( image , index ) = > ( { . . . image , label : ` 商品图 ${ index + 1 } ` } ) ) ,
. . . cloneReferenceImages . map ( ( image , index ) = > ( { . . . image , label : ` 参考图 ${ index + 1 } ` } ) ) ,
] ;
const selectedProductSetOutput =
productSetOutputOptions . find ( ( option ) = > option . key === productSetOutput ) ? ? productSetOutputOptions [ 0 ] ! ;
const selectedCloneOutput = cloneOutputOptions . find ( ( option ) = > option . key === cloneOutput ) ? ? cloneOutputOptions [ 1 ] ! ;
const productSetPreviewReady = productSetStatus === "done" ;
const cloneSetTotal = Object . values ( cloneSetCounts ) . reduce ( ( sum , value ) = > sum + value , 0 ) ;
const canGenerateSet = setImages . length > 0 && productSetStatus !== "generating" ;
2026-06-03 20:19:07 +08:00
const canGenerate = ( cloneOutput === "video-outfit"
? videoOutfitVideoFile && videoOutfitRefFile
: productImages.length > 0 ) && status !== "generating" ;
2026-06-02 12:38:01 +08:00
const canGenerateTryOn = garmentImages . length > 0 && tryOnStatus !== "generating" && tryOnStatus !== "modeling" ;
const canGenerateDetail = detailProductImages . length > 0 && detailStatus !== "generating" ;
const cloneVideoDurationProgress =
( ( cloneVideoDuration - cloneVideoDurationMin ) / ( cloneVideoDurationMax - cloneVideoDurationMin ) ) * 100 ;
const cloneVideoDurationStyle = {
"--clone-video-duration-progress" : ` ${ cloneVideoDurationProgress } % ` ,
} as CSSProperties ;
const syncRequirementMentionQuery = ( value : string , selectionStart : number | null | undefined ) = > {
setRequirementImageMentionQuery ( ecommerceMentionImages . length ? getImageMentionQuery ( value , selectionStart ) : null ) ;
} ;
const insertRequirementImageMention = ( image : MentionImageOption ) = > {
const textarea = requirementTextareaRef . current ;
const cursor = textarea ? . selectionStart ? ? requirement . length ;
const next = insertImageMentionValue ( requirement , cursor , image . name , 500 ) ;
setRequirement ( next . value ) ;
setRequirementImageMentionQuery ( null ) ;
window . requestAnimationFrame ( ( ) = > {
requirementTextareaRef . current ? . focus ( ) ;
requirementTextareaRef . current ? . setSelectionRange ( next . selectionStart , next . selectionStart ) ;
} ) ;
} ;
const addSetImages = ( files : File [ ] ) = > {
if ( setImages . length >= 3 ) return ;
2026-06-03 20:19:07 +08:00
const imageFiles = notifyRejectedImages ( files ) ;
2026-06-02 12:38:01 +08:00
if ( ! imageFiles . length ) return ;
setSetImages ( ( current ) = > {
const nextImages = createObjectImageItems ( imageFiles , 3 - current . length , "set" ) ;
return nextImages . length ? [ . . . current , . . . nextImages ] . slice ( 0 , 3 ) : current ;
} ) ;
setProductSetStatus ( "ready" ) ;
} ;
const handleSetUpload = ( event : ChangeEvent < HTMLInputElement > ) = > {
const files = event . target . files ;
if ( ! files ? . length ) return ;
addSetImages ( Array . from ( files ) ) ;
event . target . value = "" ;
} ;
const handleSetDrop = ( event : DragEvent < HTMLButtonElement > ) = > {
event . preventDefault ( ) ;
setIsSetUploadDragging ( false ) ;
const files = Array . from ( event . dataTransfer . files ) ;
if ( files . length ) addSetImages ( files ) ;
} ;
const removeSetImage = ( imageId : string ) = > {
setSetImages ( ( current ) = > {
const next = current . filter ( ( item ) = > item . id !== imageId ) ;
if ( next . length === 0 ) setProductSetStatus ( "idle" ) ;
return next ;
} ) ;
} ;
const addProductImages = ( files : File [ ] ) = > {
2026-06-03 20:19:07 +08:00
const imageFiles = notifyRejectedImages ( files ) ;
2026-06-02 12:38:01 +08:00
if ( ! imageFiles . length ) return ;
setProductImages ( ( current ) = > {
if ( current . length >= maxCloneProductImages ) return current ;
const nextImages = createObjectImageItems ( imageFiles , maxCloneProductImages - current . length , "product" ) ;
return nextImages . length ? [ . . . current , . . . nextImages ] . slice ( 0 , maxCloneProductImages ) : current ;
} ) ;
setStatus ( "ready" ) ;
setResults ( [ ] ) ;
} ;
const handleProductUpload = ( event : ChangeEvent < HTMLInputElement > ) = > {
const files = event . target . files ;
if ( ! files ? . length ) return ;
addProductImages ( Array . from ( files ) ) ;
event . target . value = "" ;
} ;
const handleProductDrop = ( event : DragEvent < HTMLDivElement > ) = > {
event . preventDefault ( ) ;
setIsProductUploadDragging ( false ) ;
const files = Array . from ( event . dataTransfer . files ) ;
if ( files . length ) addProductImages ( files ) ;
} ;
const removeProductImage = ( imageId : string ) = > {
setProductImages ( ( current ) = > {
const next = current . filter ( ( item ) = > item . id !== imageId ) ;
if ( next . length === 0 ) {
setStatus ( "idle" ) ;
setResults ( [ ] ) ;
}
return next ;
} ) ;
} ;
const hydrateCloneReferenceImageMeta = ( items : CloneImageItem [ ] ) = > {
items . forEach ( ( item ) = > {
readImageDimensions ( item . src )
. then ( ( { width , height } ) = > {
setCloneReferenceImages ( ( current ) = >
current . map ( ( currentItem ) = > ( currentItem . id === item . id ? { . . . currentItem , width , height } : currentItem ) ) ,
) ;
} )
. catch ( ( ) = > undefined ) ;
} ) ;
} ;
const addCloneReferenceImages = ( files : File [ ] ) = > {
2026-06-03 20:19:07 +08:00
const imageFiles = notifyRejectedImages ( files ) ;
2026-06-02 12:38:01 +08:00
if ( ! imageFiles . length ) return ;
const remainingSlots = maxCloneReferenceImages - cloneReferenceImages . length ;
if ( remainingSlots <= 0 ) return ;
const nextImages = createObjectImageItems ( imageFiles , remainingSlots , "reference" ) ;
if ( ! nextImages . length ) return ;
setCloneReferenceImages ( ( current ) = > {
if ( current . length >= maxCloneReferenceImages ) return current ;
return nextImages . length ? [ . . . current , . . . nextImages ] . slice ( 0 , maxCloneReferenceImages ) : current ;
} ) ;
hydrateCloneReferenceImageMeta ( nextImages ) ;
} ;
const handleCloneReferenceUpload = ( event : ChangeEvent < HTMLInputElement > ) = > {
const files = event . target . files ;
if ( ! files ? . length ) return ;
addCloneReferenceImages ( Array . from ( files ) ) ;
event . target . value = "" ;
} ;
const updateCloneSetCount = ( key : CloneSetCountKey , delta : - 1 | 1 ) = > {
setCloneSetCounts ( ( current ) = > {
const total = Object . values ( current ) . reduce ( ( sum , value ) = > sum + value , 0 ) ;
const nextValue = current [ key ] + delta ;
if ( delta < 0 && ( current [ key ] <= 0 || total <= minCloneSetTotal ) ) return current ;
if ( delta > 0 && total >= maxCloneSetTotal ) return current ;
return { . . . current , [ key ] : Math . max ( 0 , Math . min ( maxCloneSetTotal , nextValue ) ) } ;
} ) ;
} ;
const clearCloneSetCountHold = ( ) = > {
if ( countHoldTimeoutRef . current !== null ) {
window . clearTimeout ( countHoldTimeoutRef . current ) ;
countHoldTimeoutRef . current = null ;
}
if ( countHoldIntervalRef . current !== null ) {
window . clearInterval ( countHoldIntervalRef . current ) ;
countHoldIntervalRef . current = null ;
}
} ;
const startCloneSetCountHold = ( key : CloneSetCountKey , delta : - 1 | 1 , disabled : boolean ) = > {
if ( disabled ) return ;
clearCloneSetCountHold ( ) ;
updateCloneSetCount ( key , delta ) ;
window . addEventListener ( "pointerup" , clearCloneSetCountHold , { once : true } ) ;
window . addEventListener ( "pointercancel" , clearCloneSetCountHold , { once : true } ) ;
countHoldTimeoutRef . current = window . setTimeout ( ( ) = > {
countHoldIntervalRef . current = window . setInterval ( ( ) = > updateCloneSetCount ( key , delta ) , 110 ) ;
} , 320 ) ;
} ;
const toggleCloneDetailModule = ( moduleId : string ) = > {
setSelectedCloneDetailModules ( ( current ) = >
current . includes ( moduleId ) ? current . filter ( ( item ) = > item !== moduleId ) : [ . . . current , moduleId ] ,
) ;
} ;
const toggleCloneModelScene = ( scene : string ) = > {
setSelectedCloneModelScenes ( ( current ) = > ( current [ 0 ] === scene ? [ ] : [ scene ] ) ) ;
} ;
const handleProductSetPlatformChange = ( nextPlatform : string ) = > {
const normalizedPlatform = normalizePlatform ( nextPlatform ) ;
setProductSetPlatform ( normalizedPlatform ) ;
setProductSetRatio ( ( current ) = > normalizeRatioForPlatform ( normalizedPlatform , current , productSetOutput ) ) ;
setProductSetLanguage ( getPlatformDefaultLanguage ( normalizedPlatform , productSetMarket ) ) ;
} ;
const handleProductSetOutputChange = ( nextOutput : ProductSetOutputKey ) = > {
setProductSetOutput ( nextOutput ) ;
setProductSetRatio ( ( current ) = > normalizeRatioForPlatform ( productSetPlatform , current , nextOutput ) ) ;
} ;
const handleProductSetMarketChange = ( nextMarket : string ) = > {
const normalizedMarket = normalizeMarket ( nextMarket ) ;
setProductSetMarket ( normalizedMarket ) ;
setProductSetLanguage ( getPlatformDefaultLanguage ( productSetPlatform , normalizedMarket ) ) ;
} ;
const handleClonePlatformChange = ( nextPlatform : string ) = > {
const normalizedPlatform = normalizePlatform ( nextPlatform ) ;
setPlatform ( normalizedPlatform ) ;
setRatio ( ( current ) = >
cloneOutput === "hot" && current . startsWith ( "上传图片" ) && hotUploadedRatioOption
? hotUploadedRatioOption
: normalizeRatioForPlatform ( normalizedPlatform , current , cloneOutput ) ,
) ;
setLanguage ( getPlatformDefaultLanguage ( normalizedPlatform , market ) ) ;
} ;
const handleCloneOutputChange = ( nextOutput : CloneOutputKey ) = > {
setCloneOutput ( nextOutput ) ;
setRatio ( ( current ) = >
nextOutput === "hot" && current . startsWith ( "上传图片" ) && hotUploadedRatioOption
? hotUploadedRatioOption
: normalizeRatioForPlatform ( platform , current , nextOutput ) ,
) ;
} ;
const handleCloneMarketChange = ( nextMarket : string ) = > {
const normalizedMarket = normalizeMarket ( nextMarket ) ;
setMarket ( normalizedMarket ) ;
setLanguage ( getPlatformDefaultLanguage ( platform , normalizedMarket ) ) ;
} ;
const handleDetailPlatformChange = ( nextPlatform : string ) = > {
const normalizedPlatform = normalizePlatform ( nextPlatform ) ;
setDetailPlatform ( normalizedPlatform ) ;
setDetailLanguage ( getPlatformDefaultLanguage ( normalizedPlatform , detailMarket ) ) ;
} ;
const handleDetailMarketChange = ( nextMarket : string ) = > {
const normalizedMarket = normalizeMarket ( nextMarket ) ;
setDetailMarket ( normalizedMarket ) ;
setDetailLanguage ( getPlatformDefaultLanguage ( detailPlatform , normalizedMarket ) ) ;
} ;
const createCloneSettingSnapshot = ( name : string , id = ` clone-setting- ${ Date . now ( ) } ` ) : CloneSavedSetting = > ( {
id ,
name ,
savedAt : new Date ( ) . toISOString ( ) ,
output : cloneOutput ,
platform ,
market ,
language ,
ratio ,
setCounts : { . . . cloneSetCounts } ,
detailModules : [ . . . selectedCloneDetailModules ] ,
modelPanelTab : cloneModelPanelTab ,
modelScenes : [ . . . selectedCloneModelScenes ] ,
modelCustomScene : cloneModelCustomScene ,
modelGender : cloneModelGender ,
modelAge : cloneModelAge ,
modelEthnicity : cloneModelEthnicity ,
modelBody : cloneModelBody ,
modelAppearance : cloneModelAppearance ,
videoQuality : cloneVideoQuality ,
videoDurationSeconds : cloneVideoDuration ,
videoSmart : cloneVideoSmart ,
referenceMode : cloneReferenceMode ,
replicateLevel : cloneReplicateLevel ,
requirement ,
} ) ;
const persistLatestCloneSetting = ( ) = > {
const snapshot = createCloneSettingSnapshot ( cloneSettingName , "clone-setting-latest" ) ;
latestCloneSettingRef . current = snapshot ;
writeCloneLatestSetting ( snapshot ) ;
return snapshot ;
} ;
const applyCloneSavedSetting = ( setting : CloneSavedSetting ) = > {
const nextCounts = {
selling : Number.isFinite ( setting . setCounts ? . selling ) ? setting.setCounts.selling : defaultCloneSetCounts.selling ,
white : Number.isFinite ( setting . setCounts ? . white ) ? setting.setCounts.white : defaultCloneSetCounts.white ,
scene : Number.isFinite ( setting . setCounts ? . scene ) ? setting.setCounts.scene : defaultCloneSetCounts.scene ,
} ;
const nextPlatform = normalizePlatform ( setting . platform ) ;
const nextMarket = normalizeMarket ( setting . market ) ;
const nextOutput = cloneOutputOptions . some ( ( option ) = > option . key === setting . output ) ? setting . output : "detail" ;
setCloneOutput ( nextOutput ) ;
setPlatform ( nextPlatform ) ;
setMarket ( nextMarket ) ;
setLanguage ( normalizeLanguageForPlatform ( nextPlatform , nextMarket , setting . language ) ) ;
setRatio ( normalizeRatioForPlatform ( nextPlatform , setting . ratio , nextOutput ) ) ;
setCloneSetCounts ( nextCounts ) ;
setSelectedCloneDetailModules ( setting . detailModules ? . length ? setting.detailModules : defaultCloneDetailModuleIds ) ;
setCloneModelPanelTab ( setting . modelPanelTab === "model" ? "model" : "scene" ) ;
setSelectedCloneModelScenes ( normalizeCloneModelSceneSelection ( setting . modelScenes ) ) ;
setCloneModelCustomScene ( setting . modelCustomScene ? ? "" ) ;
setCloneModelGender ( tryOnModelOptions . gender . includes ( setting . modelGender ) ? setting.modelGender : tryOnModelOptions.gender [ 0 ] ) ;
setCloneModelAge ( tryOnModelOptions . age . includes ( setting . modelAge ) ? setting.modelAge : tryOnModelOptions.age [ 0 ] ) ;
setCloneModelEthnicity (
tryOnModelOptions . ethnicity . includes ( setting . modelEthnicity ) ? setting.modelEthnicity : tryOnModelOptions.ethnicity [ 0 ] ,
) ;
setCloneModelBody ( tryOnModelOptions . body . includes ( setting . modelBody ) ? setting.modelBody : tryOnModelOptions.body [ 0 ] ) ;
setCloneModelAppearance ( setting . modelAppearance ? ? "" ) ;
setCloneVideoQuality (
cloneVideoQualityOptions . some ( ( option ) = > option . key === setting . videoQuality ) ? setting . videoQuality : "high" ,
) ;
setCloneVideoDuration ( clampCloneVideoDuration ( setting . videoDurationSeconds ) ) ;
setCloneVideoSmart ( Boolean ( setting . videoSmart ) ) ;
setCloneReferenceMode ( setting . referenceMode === "link" ? "link" : "upload" ) ;
setCloneReplicateLevel ( setting . replicateLevel === "style" ? "style" : "high" ) ;
setRequirement ( ( setting . requirement ? ? "" ) . slice ( 0 , 500 ) ) ;
setCloneSettingName ( setting . name ) ;
latestCloneSettingRef . current = setting ;
writeCloneLatestSetting ( setting ) ;
} ;
useEffect ( ( ) = > {
latestCloneSettingRef . current = createCloneSettingSnapshot ( cloneSettingName , "clone-setting-latest" ) ;
} ) ;
useEffect ( ( ) = > {
const latestSetting = readCloneLatestSetting ( ) ;
if ( ! latestSetting ) return ;
skipNextCloneAutoSaveRef . current = true ;
applyCloneSavedSetting ( latestSetting ) ;
} , [ ] ) ;
useEffect ( ( ) = > {
setProductSetRatio ( ( current ) = > normalizeRatioForPlatform ( productSetPlatform , current , productSetOutput ) ) ;
} , [ productSetOutput , productSetPlatform ] ) ;
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 ;
const normalizedRatio = normalizeRatioToken ( current ) ;
const matchedRatio = availableRatios . find ( ( option ) = > normalizeRatioToken ( option ) . includes ( normalizedRatio ) ) ;
return matchedRatio ? ? getPlatformDefaultRatio ( platform , cloneOutput ) ;
} ) ;
} , [ cloneOutput , hotUploadedRatioOption , platform ] ) ;
useEffect ( ( ) = > {
if ( skipInitialCloneAutoSaveRef . current ) {
skipInitialCloneAutoSaveRef . current = false ;
return undefined ;
}
if ( skipNextCloneAutoSaveRef . current ) {
skipNextCloneAutoSaveRef . current = false ;
return undefined ;
}
const timeoutId = window . setTimeout ( ( ) = > {
persistLatestCloneSetting ( ) ;
} , 300 ) ;
return ( ) = > window . clearTimeout ( timeoutId ) ;
} , [
activeTool ,
cloneOutput ,
platform ,
market ,
language ,
ratio ,
cloneSetCounts ,
selectedCloneDetailModules ,
cloneModelPanelTab ,
selectedCloneModelScenes ,
cloneModelCustomScene ,
cloneModelGender ,
cloneModelAge ,
cloneModelEthnicity ,
cloneModelBody ,
cloneModelAppearance ,
cloneVideoQuality ,
cloneVideoDuration ,
cloneVideoSmart ,
cloneReferenceMode ,
cloneReplicateLevel ,
requirement ,
cloneSettingName ,
] ) ;
useEffect ( ( ) = > {
const persistSnapshot = ( ) = > {
if ( latestCloneSettingRef . current ) writeCloneLatestSetting ( latestCloneSettingRef . current ) ;
} ;
const handleVisibilityChange = ( ) = > {
if ( document . visibilityState === "hidden" ) persistSnapshot ( ) ;
} ;
window . addEventListener ( "pagehide" , persistSnapshot ) ;
document . addEventListener ( "visibilitychange" , handleVisibilityChange ) ;
return ( ) = > {
persistSnapshot ( ) ;
window . removeEventListener ( "pagehide" , persistSnapshot ) ;
document . removeEventListener ( "visibilitychange" , handleVisibilityChange ) ;
} ;
} , [ ] ) ;
useEffect ( ( ) = > clearCloneSetCountHold , [ ] ) ;
useEffect ( ( ) = > {
if ( ! openCloneBasicSelect ) return undefined ;
const handlePointerDown = ( event : PointerEvent ) = > {
const target = event . target ;
if ( ! ( target instanceof Element ) || target . closest ( "[data-clone-basic-select]" ) ) return ;
setOpenCloneBasicSelect ( null ) ;
} ;
const handleKeyDown = ( event : KeyboardEvent ) = > {
if ( event . key === "Escape" ) setOpenCloneBasicSelect ( null ) ;
} ;
document . addEventListener ( "pointerdown" , handlePointerDown ) ;
document . addEventListener ( "keydown" , handleKeyDown ) ;
return ( ) = > {
document . removeEventListener ( "pointerdown" , handlePointerDown ) ;
document . removeEventListener ( "keydown" , handleKeyDown ) ;
} ;
} , [ openCloneBasicSelect ] ) ;
useEffect ( ( ) = > {
if ( ! openCloneModelSelect ) return undefined ;
const handlePointerDown = ( event : PointerEvent ) = > {
const target = event . target ;
if ( ! ( target instanceof Element ) || target . closest ( "[data-clone-model-select]" ) ) return ;
setOpenCloneModelSelect ( null ) ;
setCloneModelSelectDropUp ( false ) ;
} ;
const handleKeyDown = ( event : KeyboardEvent ) = > {
if ( event . key === "Escape" ) {
setOpenCloneModelSelect ( null ) ;
setCloneModelSelectDropUp ( false ) ;
}
} ;
document . addEventListener ( "pointerdown" , handlePointerDown ) ;
document . addEventListener ( "keydown" , handleKeyDown ) ;
return ( ) = > {
document . removeEventListener ( "pointerdown" , handlePointerDown ) ;
document . removeEventListener ( "keydown" , handleKeyDown ) ;
} ;
} , [ openCloneModelSelect ] ) ;
const handleGarmentUpload = ( event : ChangeEvent < HTMLInputElement > ) = > {
const files = event . target . files ;
if ( ! files ? . length ) return ;
2026-06-03 20:19:07 +08:00
const uploadedFiles = notifyRejectedImages ( Array . from ( files ) ) ;
if ( ! uploadedFiles . length ) {
event . target . value = "" ;
return ;
}
2026-06-02 12:38:01 +08:00
setGarmentImages ( ( current ) = > [ . . . current , . . . createObjectImageItems ( uploadedFiles , 5 - current . length , "garment" ) ] . slice ( 0 , 5 ) ) ;
setTryOnStatus ( "ready" ) ;
event . target . value = "" ;
} ;
const handleDetailUpload = ( event : ChangeEvent < HTMLInputElement > ) = > {
const files = event . target . files ;
if ( ! files ? . length ) return ;
2026-06-03 20:19:07 +08:00
const uploadedFiles = notifyRejectedImages ( Array . from ( files ) ) ;
if ( ! uploadedFiles . length ) {
event . target . value = "" ;
return ;
}
2026-06-02 12:38:01 +08:00
setDetailProductImages ( ( current ) = > [ . . . current , . . . createObjectImageItems ( uploadedFiles , 3 - current . length , "detail" ) ] . slice ( 0 , 3 ) ) ;
setDetailStatus ( "ready" ) ;
event . target . value = "" ;
} ;
2026-06-03 20:19:07 +08:00
const blobToDataUrl = ( blob : Blob ) : Promise < string > = >
new Promise ( ( resolve , reject ) = > {
const reader = new FileReader ( ) ;
reader . onload = ( ) = > resolve ( String ( reader . result || "" ) ) ;
reader . onerror = ( ) = > reject ( reader . error || new Error ( "文件读取失败" ) ) ;
reader . readAsDataURL ( blob ) ;
} ) ;
2026-06-02 12:38:01 +08:00
2026-06-02 21:19:52 +08:00
const uploadCloneImages = async ( images : CloneImageItem [ ] ) : Promise < string [ ] > = > {
const urls : string [ ] = [ ] ;
for ( const item of images ) {
try {
const resp = await fetch ( item . src ) ;
const rawBlob = await resp . blob ( ) ;
2026-06-03 20:19:07 +08:00
const mimeType = normalizeEcommerceImageMime ( rawBlob . type ) ;
2026-06-02 21:19:52 +08:00
const blob = rawBlob . type === mimeType ? rawBlob : new Blob ( [ rawBlob ] , { type : mimeType } ) ;
2026-06-03 20:19:07 +08:00
const dataUrl = await blobToDataUrl ( blob ) ;
const { url } = await aiGenerationClient . uploadAsset ( { dataUrl , name : item.name , mimeType , scope : "ecommerce-product" } ) ;
2026-06-02 21:19:52 +08:00
urls . push ( url ) ;
} catch {
// skip images that fail to upload
}
}
return urls ;
} ;
const IMAGE_MODEL = "gpt-image-2" ;
2026-06-02 22:09:12 +08:00
const setCountLabels : Record < CloneSetCountKey , { label : string ; promptDesc : string } > = {
selling : { label : "卖点图" , promptDesc : "selling-point infographic image highlighting core product advantages and detail close-ups" } ,
white : { label : "白底图" , promptDesc : "clean white-background product photo showing the item from its best angle, studio lighting, no props" } ,
scene : { label : "场景图" , promptDesc : "lifestyle scene image showing the product in a realistic usage environment with natural surroundings" } ,
} ;
const buildSetSubPrompt = ( countKey : CloneSetCountKey , index : number , totalCount : number , pPlatform : string , pRatio : string , pLanguage : string , pMarket : string ) : string = > {
const info = setCountLabels [ countKey ] ;
const parts : string [ ] = [ ] ;
parts . push ( ` Generate an e-commerce ${ info . label . toLowerCase ( ) } for a product listing. ` ) ;
parts . push ( info . promptDesc ) ;
if ( totalCount > 1 ) {
parts . push ( ` This is variant ${ index + 1 } of ${ totalCount } — vary the angle, composition, or emphasis to make each distinct. ` ) ;
}
parts . push ( ` Platform: ${ pPlatform } . Aspect ratio: ${ pRatio } . Language/copy: ${ pLanguage } . Market: ${ pMarket } . ` ) ;
parts . push ( "Must comply with platform image guidelines — proper margins, no watermark, professional quality." ) ;
return parts . join ( " " ) ;
} ;
2026-06-03 20:19:07 +08:00
const buildEcommerceImagePrompt = (
outputKey : CloneOutputKey , userText : string ,
pPlatform : string , pRatio : string , pLanguage : string , pMarket : string ,
tryOnOptions ? : { gender? : string ; age? : string ; ethnicity? : string ; body? : string ; appearance? : string ; scenes? : string [ ] ; smartScene? : boolean } ,
) : string = > {
2026-06-02 21:19:52 +08:00
const parts : string [ ] = [ ] ;
2026-06-02 22:09:12 +08:00
if ( outputKey === "detail" ) {
parts . push ( "Generate a professional A+ detail page hero image for an e-commerce product listing." ) ;
parts . push ( "Create a high-impact first-screen visual that combines the product photo with key selling points, usage scenes, and detailed specifications in a cohesive layout." ) ;
2026-06-02 21:19:52 +08:00
parts . push ( ` Platform: ${ pPlatform } . Aspect ratio: ${ pRatio } . Language/copy: ${ pLanguage } . Market: ${ pMarket } . ` ) ;
2026-06-02 22:09:12 +08:00
parts . push ( "Follow platform A+ page best practices — clear hierarchy, professional typography, high visual impact." ) ;
2026-06-02 21:19:52 +08:00
} else if ( outputKey === "model" ) {
parts . push ( "Generate model/try-on lifestyle images for an e-commerce product listing." ) ;
2026-06-02 22:09:12 +08:00
parts . push ( "Show the product being used or worn by a model in attractive lifestyle settings." ) ;
2026-06-02 21:19:52 +08:00
parts . push ( ` Platform: ${ pPlatform } . Aspect ratio: ${ pRatio } . Language/copy: ${ pLanguage } . Market: ${ pMarket } . ` ) ;
2026-06-03 20:19:07 +08:00
if ( tryOnOptions ) {
if ( tryOnOptions . gender ) parts . push ( ` Model gender: ${ tryOnOptions . gender } . ` ) ;
if ( tryOnOptions . age ) parts . push ( ` Model age: ${ tryOnOptions . age } . ` ) ;
if ( tryOnOptions . ethnicity ) parts . push ( ` Model ethnicity: ${ tryOnOptions . ethnicity } . ` ) ;
if ( tryOnOptions . body ) parts . push ( ` Model body type: ${ tryOnOptions . body } . ` ) ;
if ( tryOnOptions . appearance ) parts . push ( ` Model appearance details: ${ tryOnOptions . appearance } . ` ) ;
if ( tryOnOptions . scenes ? . length ) parts . push ( ` Background scenes: ${ tryOnOptions . scenes . join ( ", " ) } . ` ) ;
if ( tryOnOptions . smartScene ) parts . push ( "Use smart scene matching to select the best background context." ) ;
}
2026-06-02 21:19:52 +08:00
parts . push ( "Model should appear natural and appealing. Background should complement the product. Image must meet platform standards." ) ;
} else if ( outputKey === "hot" ) {
parts . push ( "Generate a high-conversion e-commerce product image that closely replicates the style and composition of the reference image while adapting it to the target platform." ) ;
parts . push ( ` Replicate the visual style, color palette, and layout feel of the source product image, then adapt it for ${ pPlatform } marketplace standards. ` ) ;
parts . push ( ` Platform: ${ pPlatform } . Aspect ratio: ${ pRatio } . Language/copy: ${ pLanguage } . Market: ${ pMarket } . ` ) ;
parts . push ( "The result must look professional and optimized for high click-through rate and conversion on the specified platform." ) ;
}
if ( userText . trim ( ) ) {
parts . push ( ` Additional user requirements: ${ userText . trim ( ) } ` ) ;
}
return parts . join ( " " ) ;
} ;
2026-06-02 22:09:12 +08:00
const generateSetImages = async (
images : CloneImageItem [ ] ,
counts : Record < CloneSetCountKey , number > ,
userText : string ,
pPlatform : string ,
pRatio : string ,
pLanguage : string ,
pMarket : string ,
setStatusFn : ( status : "generating" | "done" | "idle" ) = > void ,
setResultFn : ( urls : string [ ] ) = > void ,
) : Promise < void > = > {
setStatusFn ( "generating" ) ;
try {
const referenceUrls = await uploadCloneImages ( images ) ;
if ( ! referenceUrls . length ) {
setStatusFn ( "idle" ) ;
return ;
}
const generatedUrls : string [ ] = [ ] ;
const stamp = Date . now ( ) ;
for ( const countKey of cloneSetCountOptions . map ( ( o ) = > o . key ) ) {
const count = counts [ countKey ] ;
for ( let i = 0 ; i < count ; i ++ ) {
if ( imageAbortRef . current . current ) break ;
const subPrompt = buildSetSubPrompt ( countKey , i , count , pPlatform , pRatio , pLanguage , pMarket ) ;
const fullPrompt = userText . trim ( ) ? ` ${ subPrompt } Additional user requirements: ${ userText . trim ( ) } ` : subPrompt ;
const { taskId } = await aiGenerationClient . createImageTask ( {
model : IMAGE_MODEL ,
prompt : fullPrompt ,
ratio : pRatio ,
quality : pRatio.includes ( "720" ) ? "720P" : "1080P" ,
gridMode : "single" ,
referenceUrls ,
} ) ;
2026-06-03 20:19:07 +08:00
const storeId = imageGen . submitTask ( { title : ` ${ setCountLabels [ countKey ] . label } ${ i + 1 } ` , type : "image" , status : "running" , progress : 5 , prompt : fullPrompt , sourceView : "ecommerce" , taskId } ) ;
2026-06-02 22:09:12 +08:00
const resultUrl = await waitForTask ( taskId , {
abortRef : imageAbortRef.current ,
onProgress : ( ) = > { } ,
} ) ;
if ( resultUrl ) {
generatedUrls . push ( resultUrl ) ;
2026-06-03 20:19:07 +08:00
imageGen . updateTask ( storeId , { status : "completed" , progress : 100 , resultUrl } ) ;
2026-06-02 22:09:12 +08:00
} else {
generatedUrls . push ( "" ) ;
2026-06-03 20:19:07 +08:00
imageGen . updateTask ( storeId , { status : "failed" , error : "生成未返回结果" } ) ;
2026-06-02 22:09:12 +08:00
}
}
}
setResultFn ( generatedUrls ) ;
setStatusFn ( generatedUrls . some ( Boolean ) ? "done" : "idle" ) ;
} catch ( err ) {
if ( err instanceof ServerRequestError && err . status === 402 ) {
setResultFn ( [ ] ) ;
2026-06-03 20:19:07 +08:00
toast . error ( "余额不足,请充值后继续" ) ;
} else {
const msg = err instanceof Error ? err . message : "生成失败" ;
toast . error ( msg ) ;
2026-06-02 22:09:12 +08:00
}
2026-06-03 20:19:07 +08:00
setStatusFn ( "failed" ) ;
2026-06-02 22:09:12 +08:00
}
} ;
2026-06-02 21:19:52 +08:00
const generateEcommerceImage = async (
outputKey : CloneOutputKey ,
images : CloneImageItem [ ] ,
userText : string ,
pPlatform : string ,
pRatio : string ,
pLanguage : string ,
pMarket : string ,
2026-06-03 20:19:07 +08:00
tryOnOptions ? : { gender? : string ; age? : string ; ethnicity? : string ; body? : string ; appearance? : string ; scenes? : string [ ] ; smartScene? : boolean } ,
statusFn ? : ( status : "generating" | "done" | "idle" | "failed" ) = > void ,
resultFn ? : ( results : CloneImageItem [ ] ) = > void ,
2026-06-02 21:19:52 +08:00
) : Promise < void > = > {
setStatusFn ( "generating" ) ;
try {
const referenceUrls = await uploadCloneImages ( images ) ;
if ( ! referenceUrls . length ) {
setStatusFn ( "idle" ) ;
return ;
}
2026-06-03 20:19:07 +08:00
const prompt = buildEcommerceImagePrompt ( outputKey , userText , pPlatform , pRatio , pLanguage , pMarket , tryOnOptions ) ;
2026-06-02 21:19:52 +08:00
const stamp = Date . now ( ) ;
const { taskId } = await aiGenerationClient . createImageTask ( {
model : IMAGE_MODEL ,
prompt ,
ratio : pRatio ,
quality : pRatio.includes ( "720" ) ? "720P" : "1080P" ,
2026-06-02 22:09:12 +08:00
gridMode : "single" ,
2026-06-02 21:19:52 +08:00
referenceUrls ,
} ) ;
2026-06-03 20:19:07 +08:00
const storeId = imageGen . submitTask ( { title : ` 电商 ${ outputKey } 图 ` , type : "image" , status : "running" , progress : 5 , prompt , sourceView : "ecommerce" , taskId } ) ;
2026-06-02 21:19:52 +08:00
const resultUrl = await waitForTask ( taskId , {
abortRef : imageAbortRef.current ,
onProgress : ( ) = > { } ,
} ) ;
if ( resultUrl ) {
2026-06-02 22:09:12 +08:00
setResultFn ( [ { id : ` ecommerce- ${ stamp } ` , src : resultUrl , label : selectedCloneOutput.label } ] ) ;
2026-06-02 21:19:52 +08:00
setStatusFn ( "done" ) ;
2026-06-03 20:19:07 +08:00
imageGen . updateTask ( storeId , { status : "completed" , progress : 100 , resultUrl } ) ;
2026-06-02 21:19:52 +08:00
} else {
setStatusFn ( "idle" ) ;
2026-06-03 20:19:07 +08:00
imageGen . updateTask ( storeId , { status : "failed" , error : "生成未返回结果" } ) ;
2026-06-02 21:19:52 +08:00
}
} catch ( err ) {
if ( err instanceof ServerRequestError && err . status === 402 ) {
2026-06-02 22:09:12 +08:00
setResultFn ( [ { id : ` ecommerce-error-402 ` , src : "" , label : "余额不足,请充值后继续" } ] ) ;
2026-06-03 20:19:07 +08:00
toast . error ( "余额不足,请充值后继续" ) ;
} else {
const msg = err instanceof Error ? err . message : "生成失败" ;
toast . error ( msg ) ;
2026-06-02 21:19:52 +08:00
}
2026-06-03 20:19:07 +08:00
setStatusFn ( "failed" ) ;
2026-06-02 21:19:52 +08:00
}
} ;
2026-06-03 20:19:07 +08:00
const handleVideoOutfitGenerate = async ( ) = > {
if ( ! videoOutfitVideoFile || ! videoOutfitRefFile ) return ;
setStatus ( "generating" ) ;
2026-06-02 12:38:01 +08:00
try {
2026-06-03 20:19:07 +08:00
const readAsDataUrl = ( file : File ) : Promise < string > = > new Promise ( ( resolve , reject ) = > {
const reader = new FileReader ( ) ;
reader . onload = ( ) = > resolve ( reader . result as string ) ;
reader . onerror = ( ) = > reject ( new Error ( "文件读取失败" ) ) ;
reader . readAsDataURL ( file ) ;
} ) ;
2026-06-02 12:38:01 +08:00
2026-06-03 20:19:07 +08:00
const videoDataUrl = await readAsDataUrl ( videoOutfitVideoFile ) ;
const refDataUrl = await readAsDataUrl ( videoOutfitRefFile ) ;
2026-06-02 12:38:01 +08:00
2026-06-03 20:19:07 +08:00
const videoAsset = await aiGenerationClient . uploadAsset ( {
dataUrl : videoDataUrl , name : videoOutfitVideoFile.name ,
mimeType : videoOutfitVideoFile.type || "video/mp4" , scope : "video-outfit" ,
} ) ;
const refAsset = await aiGenerationClient . uploadAsset ( {
dataUrl : refDataUrl , name : videoOutfitRefFile.name ,
mimeType : videoOutfitRefFile.type || "image/png" , scope : "video-outfit" ,
} ) ;
2026-06-02 12:38:01 +08:00
2026-06-03 20:19:07 +08:00
const { taskId } = await aiGenerationClient . createVideoEditTask ( {
videoUrl : videoAsset.url ,
referenceUrls : [ refAsset . url ] ,
prompt : requirement || undefined ,
} ) ;
2026-06-02 12:38:01 +08:00
2026-06-03 20:19:07 +08:00
const { waitForTask } = await import ( "../../api/taskSubscription" ) ;
abortRef . current = { current : false } ;
const resultUrl = await waitForTask ( taskId , { abortRef : abortRef.current } ) ;
if ( resultUrl ) {
setResults ( [ { id : crypto.randomUUID ( ) , name : "换装视频" , src : resultUrl , type : "video" , size : 0 } ] ) ;
}
setStatus ( "done" ) ;
2026-06-02 12:38:01 +08:00
} catch ( err ) {
2026-06-03 20:19:07 +08:00
setStatus ( "failed" ) ;
toast . error ( err instanceof Error ? err . message : "视频换装生成失败" ) ;
2026-06-02 12:38:01 +08:00
}
} ;
2026-06-03 20:19:07 +08:00
const handleGenerate = ( ) = > {
if ( ! canGenerate ) return ;
2026-06-02 12:38:01 +08:00
2026-06-03 20:19:07 +08:00
if ( ( appUsage ? . balanceCents ? ? 0 ) <= 0 ) {
toast . error ( "积分不足,请充值后继续" ) ;
return ;
2026-06-02 12:38:01 +08:00
}
2026-06-03 20:19:07 +08:00
if ( cloneOutput === "set" && cloneSetTotal > 5 ) {
if ( ! window . confirm ( ` 将生成 ${ cloneSetTotal } 张图片,可能消耗较多积分,是否继续? ` ) ) return ;
}
2026-06-02 12:38:01 +08:00
2026-06-02 21:19:52 +08:00
imageAbortRef . current = { current : false } ;
2026-06-03 20:19:07 +08:00
lastFailedActionRef . current = null ;
if ( cloneOutput === "video-outfit" ) {
void handleVideoOutfitGenerate ( ) ;
} else if ( cloneOutput === "set" ) {
2026-06-02 22:09:12 +08:00
void generateSetImages (
productImages , cloneSetCounts , requirement ,
platform , ratio , language , market ,
( s ) = > setStatus ( s as ProductCloneStatus ) ,
( urls ) = > setProductSetResultImages ( urls ) ,
) ;
} else {
void generateEcommerceImage (
cloneOutput , productImages , requirement ,
platform , ratio , language , market ,
( s ) = > setStatus ( s as ProductCloneStatus ) , setResults ,
) ;
2026-06-03 20:19:07 +08:00
lastFailedActionRef . current = ( ) = > handleGenerate ( ) ;
2026-06-02 22:09:12 +08:00
}
2026-06-02 12:38:01 +08:00
} ;
const handleGenerateModel = ( ) = > {
2026-06-02 21:19:52 +08:00
imageAbortRef . current = { current : false } ;
2026-06-03 20:19:07 +08:00
lastFailedActionRef . current = null ;
2026-06-02 12:38:01 +08:00
setTryOnStatus ( "modeling" ) ;
2026-06-02 21:19:52 +08:00
void generateEcommerceImage (
"model" , garmentImages , requirement ,
platform , ratio , language , market ,
2026-06-03 20:19:07 +08:00
{ gender : modelGender , age : modelAge , ethnicity : modelEthnicity , body : modelBody , appearance , scenes : selectedScenes , smartScene } ,
2026-06-02 21:19:52 +08:00
( s ) = > {
if ( s === "done" ) setTryOnStatus ( "ready" ) ;
else setTryOnStatus ( s as TryOnStatus ) ;
} ,
( ) = > { setTryOnStatus ( "ready" ) ; } ,
) ;
2026-06-03 20:19:07 +08:00
lastFailedActionRef . current = ( ) = > handleGenerateModel ( ) ;
2026-06-02 12:38:01 +08:00
} ;
const handleTryOnGenerate = ( ) = > {
if ( ! canGenerateTryOn ) return ;
2026-06-02 21:19:52 +08:00
imageAbortRef . current = { current : false } ;
2026-06-03 20:19:07 +08:00
lastFailedActionRef . current = null ;
2026-06-02 21:19:52 +08:00
void generateEcommerceImage (
"model" , garmentImages , requirement ,
platform , ratio , language , market ,
2026-06-03 20:19:07 +08:00
{ gender : modelGender , age : modelAge , ethnicity : modelEthnicity , body : modelBody , appearance , scenes : selectedScenes , smartScene } ,
2026-06-02 21:19:52 +08:00
( s ) = > setTryOnStatus ( s as TryOnStatus ) ,
( res ) = > setTryOnResultImages ( res . map ( ( r ) = > r . src ) . filter ( Boolean ) ) ,
) ;
2026-06-03 20:19:07 +08:00
lastFailedActionRef . current = ( ) = > handleTryOnGenerate ( ) ;
2026-06-02 12:38:01 +08:00
} ;
const toggleScene = ( scene : string ) = > {
setSelectedScenes ( ( current ) = >
current . includes ( scene ) ? current . filter ( ( item ) = > item !== scene ) : [ . . . current , scene ] ,
) ;
} ;
const toggleDetailModule = ( moduleId : string ) = > {
setSelectedDetailModules ( ( current ) = >
current . includes ( moduleId ) ? current . filter ( ( item ) = > item !== moduleId ) : [ . . . current , moduleId ] ,
) ;
} ;
const handleSetGenerate = ( ) = > {
if ( ! canGenerateSet ) return ;
2026-06-02 21:19:52 +08:00
imageAbortRef . current = { current : false } ;
2026-06-03 20:19:07 +08:00
lastFailedActionRef . current = null ;
2026-06-02 22:09:12 +08:00
void generateSetImages (
setImages , cloneSetCounts , productSetRequirement ,
2026-06-02 21:19:52 +08:00
productSetPlatform , productSetRatio , productSetLanguage , productSetMarket ,
( s ) = > setProductSetStatus ( s as ProductSetStatus ) ,
2026-06-02 22:09:12 +08:00
( urls ) = > setProductSetResultImages ( urls ) ,
2026-06-02 21:19:52 +08:00
) ;
2026-06-03 20:19:07 +08:00
lastFailedActionRef . current = ( ) = > handleSetGenerate ( ) ;
2026-06-02 12:38:01 +08:00
} ;
const openProductSetPreview = ( card : { src : string ; label : string } ) = > {
setSelectedProductSetPreview ( card ) ;
} ;
const handleDetailAiWrite = ( ) = > {
setDetailRequirement (
"1.产品名称:无线降噪蓝牙耳机\n2.核心卖点:主动降噪、24H续航、低延迟连接、舒适佩戴\n3.适用人群:通勤、办公、运动和旅行用户\n4.期望场景:地铁通勤、居家办公、户外运动\n5.具体参数:蓝牙5.3、IPX4防水、快充10分钟使用2小时" ,
) ;
} ;
const handleDetailGenerate = ( ) = > {
if ( ! canGenerateDetail ) return ;
2026-06-02 21:19:52 +08:00
imageAbortRef . current = { current : false } ;
2026-06-03 20:19:07 +08:00
lastFailedActionRef . current = null ;
2026-06-02 21:19:52 +08:00
void generateEcommerceImage (
"detail" , detailProductImages , detailRequirement ,
detailPlatform , getPlatformDefaultRatio ( detailPlatform ) , detailLanguage , detailMarket ,
( s ) = > setDetailStatus ( s as DetailStatus ) ,
2026-06-02 21:31:43 +08:00
( res ) = > setDetailResultUrl ( res [ 0 ] ? . src ? ? null ) ,
2026-06-02 21:19:52 +08:00
) ;
2026-06-02 12:38:01 +08:00
} ;
const resetTask = ( ) = > {
setSetImages ( [ ] ) ;
setProductSetRequirement ( "" ) ;
setProductSetOutput ( "video" ) ;
setProductSetRatio ( ( current ) = > normalizeRatioForPlatform ( productSetPlatform , current , "video" ) ) ;
setProductSetStatus ( "idle" ) ;
setIsSetUploadDragging ( false ) ;
setSelectedProductSetPreview ( null ) ;
setShowHostingModal ( false ) ;
setProductImages ( [ ] ) ;
setIsProductUploadDragging ( false ) ;
setCloneOutput ( "detail" ) ;
setRatio ( ( current ) = > normalizeRatioForPlatform ( platform , current , "detail" ) ) ;
setCloneSetCounts ( defaultCloneSetCounts ) ;
setSelectedCloneDetailModules ( defaultCloneDetailModuleIds ) ;
setCloneModelPanelTab ( "scene" ) ;
setSelectedCloneModelScenes ( [ ] ) ;
setCloneModelCustomScene ( "" ) ;
setCloneModelGender ( tryOnModelOptions . gender [ 0 ] ) ;
setCloneModelAge ( tryOnModelOptions . age [ 0 ] ) ;
setCloneModelEthnicity ( tryOnModelOptions . ethnicity [ 0 ] ) ;
setCloneModelBody ( tryOnModelOptions . body [ 0 ] ) ;
setCloneModelAppearance ( "" ) ;
setCloneVideoQuality ( "high" ) ;
setCloneVideoDuration ( 10 ) ;
setCloneVideoSmart ( true ) ;
setCloneReferenceMode ( "upload" ) ;
setCloneReferenceImages ( [ ] ) ;
setCloneReplicateLevel ( "high" ) ;
setRequirement ( "" ) ;
setCloneSettingName ( "新建创作" ) ;
setResults ( [ ] ) ;
setStatus ( "idle" ) ;
setGarmentImages ( [ ] ) ;
setAppearance ( "" ) ;
setSelectedScenes ( [ ] ) ;
setCustomScene ( "" ) ;
setSmartScene ( false ) ;
setTryOnRatio ( tryOnRatioOptions [ 0 ] ) ;
setTryOnStatus ( "idle" ) ;
setTryOnResultImages ( [ ] ) ;
setDetailProductImages ( [ ] ) ;
setDetailRequirement ( "" ) ;
setSelectedDetailModules ( defaultDetailModuleIds ) ;
setDetailStatus ( "idle" ) ;
} ;
const activeToolMeta = sideTools . find ( ( tool ) = > tool . key === activeTool ) ;
const isSetTool = activeTool === "set" ;
const isDetail = activeTool === "detail" ;
const isTryOn = activeTool === "wear" ;
const isCloneTool = activeTool === "clone" ;
const pageLabel = isSetTool ? "商品套图" : isDetail ? "A+/详情页" : isTryOn ? "AI服饰穿戴" : activeToolMeta ? . label || "商品工具" ;
const setPrimaryLabel =
setImages . length === 0
? ` 请先上传商品原图 `
: productSetStatus === "generating"
? "生成中..."
: ` 生成 ${ selectedProductSetOutput . label } ` ;
const tryOnPrimaryLabel =
garmentImages . length === 0 ? "请先上传服装图片" : tryOnStatus === "generating" ? "生成中..." : "生成服饰穿戴图" ;
const detailPrimaryLabel =
detailProductImages . length === 0 ? "请上传产品图" : detailStatus === "generating" ? "生成中..." : "生成A+详情页" ;
const clonePrimaryLabel =
productImages . length === 0 ? "请先上传商品原图" : status === "generating" ? "生成中..." : ` 生成 ${ selectedCloneOutput . label } ` ;
2026-06-02 22:09:12 +08:00
const setPreviewCards : CloneResult [ ] = [ ] ;
let setIndex = 0 ;
for ( const countKey of cloneSetCountOptions . map ( ( o ) = > o . key ) ) {
const count = cloneSetCounts [ countKey ] ;
const info = setCountLabels [ countKey ] ;
for ( let i = 0 ; i < count ; i ++ ) {
setPreviewCards . push ( {
id : ` ${ countKey } - ${ i } ` ,
src : productSetResultImages [ setIndex ] ? ? productSetPreviewCards [ setIndex % productSetPreviewCards . length ] ? . src ? ? "" ,
label : ` ${ info . label } ${ count > 1 ? ` ${ i + 1 } ` : "" } ` ,
} ) ;
setIndex ++ ;
}
}
const clonePreviewCards : CloneResult [ ] = [ ] ;
let cloneIndex = 0 ;
for ( const countKey of cloneSetCountOptions . map ( ( o ) = > o . key ) ) {
const count = cloneSetCounts [ countKey ] ;
const info = setCountLabels [ countKey ] ;
for ( let i = 0 ; i < count ; i ++ ) {
clonePreviewCards . push ( {
id : ` ${ countKey } - ${ i } ` ,
src : results [ cloneIndex ] ? . src ? ? productSetPreviewCards [ cloneIndex % productSetPreviewCards . length ] ? . src ? ? "" ,
label : ` ${ info . label } ${ count > 1 ? ` ${ i + 1 } ` : "" } ` ,
} ) ;
cloneIndex ++ ;
}
}
2026-06-02 12:38:01 +08:00
const cloneBasicSelects : Array < {
key : CloneBasicSelectKey ;
label : string ;
value : string ;
options : string [ ] ;
onChange : ( value : string ) = > void ;
} > = [
{ key : "platform" , label : "平台" , value : platform , options : platformOptions , onChange : handleClonePlatformChange } ,
{ key : "market" , label : "国家" , value : market , options : marketOptions , onChange : handleCloneMarketChange } ,
{ key : "language" , label : "语言" , value : language , options : cloneLanguageOptions , onChange : setLanguage } ,
{ key : "ratio" , label : "尺寸/比例" , value : ratio , options : cloneRatioOptions , onChange : setRatio } ,
] ;
const cloneModelSelects : Array < {
key : CloneModelSelectKey ;
label : string ;
value : string ;
options : string [ ] ;
onChange : ( value : string ) = > void ;
} > = [
{ key : "gender" , label : "性别" , value : cloneModelGender , options : tryOnModelOptions.gender , onChange : setCloneModelGender } ,
{ key : "age" , label : "年龄" , value : cloneModelAge , options : tryOnModelOptions.age , onChange : setCloneModelAge } ,
{
key : "ethnicity" ,
label : "人种" ,
value : cloneModelEthnicity ,
options : tryOnModelOptions.ethnicity ,
onChange : setCloneModelEthnicity ,
} ,
{ key : "body" , label : "体型" , value : cloneModelBody , options : tryOnModelOptions.body , onChange : setCloneModelBody } ,
] ;
const setPanel = (
2026-06-03 20:19:07 +08:00
< EcommerceSetPanel
setInputRef = { setInputRef }
setImages = { setImages }
isSetUploadDragging = { isSetUploadDragging }
productSetOutputOptions = { productSetOutputOptions }
productSetOutput = { productSetOutput }
platformOptions = { platformOptions }
marketOptions = { marketOptions }
productSetLanguageOptions = { productSetLanguageOptions }
productSetRatioOptions = { productSetRatioOptions }
productSetPlatform = { productSetPlatform }
productSetMarket = { productSetMarket }
productSetLanguage = { productSetLanguage }
productSetRatio = { productSetRatio }
setIsSetUploadDragging = { setIsSetUploadDragging }
handleSetDrop = { handleSetDrop }
handleSetUpload = { handleSetUpload }
removeSetImage = { removeSetImage }
handleProductSetOutputChange = { handleProductSetOutputChange }
handleProductSetPlatformChange = { handleProductSetPlatformChange }
handleProductSetMarketChange = { handleProductSetMarketChange }
setProductSetLanguage = { setProductSetLanguage }
setProductSetRatio = { setProductSetRatio }
formatRatioDisplayValue = { formatRatioDisplayValue }
/ >
2026-06-02 12:38:01 +08:00
) ;
const clonePanel = (
2026-06-03 20:19:07 +08:00
< EcommerceClonePanel
productInputRef = { productInputRef }
cloneReferenceInputRef = { cloneReferenceInputRef }
productImages = { productImages }
isProductUploadDragging = { isProductUploadDragging }
cloneOutput = { cloneOutput }
cloneOutputOptions = { cloneOutputOptions }
cloneBasicSelects = { cloneBasicSelects }
openCloneBasicSelect = { openCloneBasicSelect }
cloneReferenceMode = { cloneReferenceMode }
cloneReferenceImages = { cloneReferenceImages }
maxCloneReferenceImages = { maxCloneReferenceImages }
cloneReplicateLevel = { cloneReplicateLevel }
cloneReplicateLevelOptions = { cloneReplicateLevelOptions }
cloneSetCounts = { cloneSetCounts }
cloneSetCountOptions = { cloneSetCountOptions }
cloneSetTotal = { cloneSetTotal }
minCloneSetTotal = { minCloneSetTotal }
maxCloneSetTotal = { maxCloneSetTotal }
selectedCloneDetailModules = { selectedCloneDetailModules }
cloneDetailModules = { cloneDetailModules }
cloneModelPanelTab = { cloneModelPanelTab }
tryOnScenes = { tryOnScenes }
selectedCloneModelScenes = { selectedCloneModelScenes }
cloneModelCustomScene = { cloneModelCustomScene }
cloneModelSelects = { cloneModelSelects }
openCloneModelSelect = { openCloneModelSelect }
cloneModelSelectDropUp = { cloneModelSelectDropUp }
cloneModelAppearance = { cloneModelAppearance }
cloneVideoQuality = { cloneVideoQuality }
cloneVideoQualityOptions = { cloneVideoQualityOptions }
cloneVideoDuration = { cloneVideoDuration }
cloneVideoDurationMin = { cloneVideoDurationMin }
cloneVideoDurationMax = { cloneVideoDurationMax }
cloneVideoDurationStyle = { cloneVideoDurationStyle }
cloneVideoSmart = { cloneVideoSmart }
canGenerate = { canGenerate }
status = { status }
lastFailedActionRef = { lastFailedActionRef }
setIsProductUploadDragging = { setIsProductUploadDragging }
handleProductDrop = { handleProductDrop }
removeProductImage = { removeProductImage }
handleProductUpload = { handleProductUpload }
handleCloneOutputChange = { handleCloneOutputChange }
setOpenCloneBasicSelect = { setOpenCloneBasicSelect }
setCloneReferenceMode = { setCloneReferenceMode }
handleCloneReferenceUpload = { handleCloneReferenceUpload }
setCloneReplicateLevel = { setCloneReplicateLevel }
startCloneSetCountHold = { startCloneSetCountHold }
clearCloneSetCountHold = { clearCloneSetCountHold }
toggleCloneDetailModule = { toggleCloneDetailModule }
setCloneModelPanelTab = { setCloneModelPanelTab }
toggleCloneModelScene = { toggleCloneModelScene }
setCloneModelCustomScene = { setCloneModelCustomScene }
setOpenCloneModelSelect = { setOpenCloneModelSelect }
setCloneModelSelectDropUp = { setCloneModelSelectDropUp }
setCloneModelAppearance = { setCloneModelAppearance }
setCloneVideoQuality = { setCloneVideoQuality }
setCloneVideoDuration = { setCloneVideoDuration }
clampCloneVideoDuration = { clampCloneVideoDuration }
setCloneVideoSmart = { setCloneVideoSmart }
handleGenerate = { handleGenerate }
formatRatioDisplayValue = { formatRatioDisplayValue }
setVideoOutfitFiles = { ( video , ref ) = > { setVideoOutfitVideoFile ( video ) ; setVideoOutfitRefFile ( ref ) ; } }
/ >
2026-06-02 12:38:01 +08:00
) ;
const detailPanel = (
2026-06-03 20:19:07 +08:00
< EcommerceDetailPanel
detailInputRef = { detailInputRef }
detailProductImages = { detailProductImages }
detailPlatform = { detailPlatform }
detailMarket = { detailMarket }
detailLanguage = { detailLanguage }
detailType = { detailType }
detailRequirement = { detailRequirement }
selectedDetailModules = { selectedDetailModules }
detailStatus = { detailStatus }
canGenerateDetail = { canGenerateDetail }
detailPrimaryLabel = { detailPrimaryLabel }
platformOptions = { platformOptions }
marketOptions = { marketOptions }
detailLanguageOptions = { detailLanguageOptions }
detailTypeOptions = { detailTypeOptions }
detailModules = { detailModules }
handleDetailUpload = { handleDetailUpload }
handleDetailPlatformChange = { handleDetailPlatformChange }
handleDetailMarketChange = { handleDetailMarketChange }
setDetailLanguage = { setDetailLanguage }
setDetailType = { setDetailType }
setDetailRequirement = { setDetailRequirement }
handleDetailAiWrite = { handleDetailAiWrite }
toggleDetailModule = { toggleDetailModule }
handleDetailGenerate = { handleDetailGenerate }
/ >
2026-06-02 12:38:01 +08:00
) ;
const tryOnPanel = (
2026-06-03 20:19:07 +08:00
< EcommerceTryOnPanel
garmentInputRef = { garmentInputRef }
garmentImages = { garmentImages }
modelSource = { modelSource }
modelGender = { modelGender }
modelAge = { modelAge }
modelEthnicity = { modelEthnicity }
modelBody = { modelBody }
appearance = { appearance }
selectedScenes = { selectedScenes }
customScene = { customScene }
smartScene = { smartScene }
tryOnRatio = { tryOnRatio }
tryOnStatus = { tryOnStatus }
canGenerateTryOn = { canGenerateTryOn }
tryOnPrimaryLabel = { tryOnPrimaryLabel }
tryOnModelOptions = { tryOnModelOptions }
tryOnAssets = { tryOnAssets }
tryOnScenes = { tryOnScenes }
tryOnRatioOptions = { tryOnRatioOptions }
handleGarmentUpload = { handleGarmentUpload }
setModelSource = { setModelSource }
setModelGender = { setModelGender }
setModelAge = { setModelAge }
setModelEthnicity = { setModelEthnicity }
setModelBody = { setModelBody }
setAppearance = { setAppearance }
handleGenerateModel = { handleGenerateModel }
toggleScene = { toggleScene }
setCustomScene = { setCustomScene }
setSmartScene = { setSmartScene }
setTryOnRatio = { setTryOnRatio }
handleTryOnGenerate = { handleTryOnGenerate }
/ >
2026-06-02 12:38:01 +08:00
) ;
const placeholderPanel = (
< >
< div className = "product-clone-panel__scroll" >
< section className = "product-clone-empty-panel" >
< span > { activeToolMeta ? . icon } < / span >
< h2 > { activeToolMeta ? . label } < / h2 >
< p > 该 工 具 页 面 正 在 接 入 , 当 前 可 使 用 电 商 AI作图 、 商 品 套 图 、 A + 详 情 与 服 饰 穿 戴 。 < / p >
< / section >
< / div >
< footer className = "product-clone-panel__footer" >
< button type = "button" className = "product-clone-primary" disabled >
暂 未 开 放
< / button >
< / footer >
< / >
) ;
const setPreview = (
< main className = "product-clone-preview product-clone-preview--set" aria-label = "AI商品套图预览" >
< div className = "product-clone-preview__headline" >
< h1 > 预 览 < / h1 >
< p >
上 传 商 品 图 , AI 即 刻 生 成 < span > 符 合 多 电 商 平 台 规 范 < / span > 的 高 转 化 率 商 品 套 图 。
< / p >
< / div >
{ productSetPreviewReady ? (
< section className = "product-set-demo-board" >
< button
type = "button"
className = "product-set-main-card"
2026-06-02 22:09:12 +08:00
onClick = { ( ) = > openProductSetPreview ( setPreviewCards [ 0 ] ? ? productSetPreviewCards [ 0 ] ) }
2026-06-02 12:38:01 +08:00
>
2026-06-02 22:09:12 +08:00
< img src = { setImages [ 0 ] ? . src ? ? ( setPreviewCards [ 0 ] ? . src ? ? productSetPreviewCards [ 0 ] . src ) } alt = "商品原图" / >
< span > 原 图 素 材 < / span >
2026-06-02 12:38:01 +08:00
< / button >
< div className = "product-set-flow-arrow" aria-hidden = "true" / >
2026-06-02 17:37:51 +08:00
< div className = "product-set-card-grid result-reveal" >
2026-06-02 22:09:12 +08:00
{ setPreviewCards . map ( ( card ) = > (
2026-06-02 12:38:01 +08:00
< button key = { card . id } type = "button" onClick = { ( ) = > openProductSetPreview ( card ) } >
< img src = { card . src } alt = { card . label } / >
< span > { card . label } < / span >
< / button >
) ) }
< / div >
< / section >
) : (
< section className = "product-set-empty-preview" aria-live = "polite" >
{ productSetStatus === "generating" ? < LoadingOutlined / > : < FileImageOutlined / > }
< strong > { productSetStatus === "generating" ? "正在生成" : "等待生成" } < / strong >
2026-06-02 17:37:51 +08:00
{ productSetStatus === "generating" ? < EcommerceProgressBar status = "generating" label = "商品套图" / > : null }
2026-06-02 12:38:01 +08:00
< span > { productSetStatus === "generating" ? "AI 正在整理主图、场景、细节与卖点图。" : "上传商品原图并填写信息后,AI 将为您生成专业的电商商品图" } < / span >
< / section >
) }
{ productSetStatus === "done" ? < p className = "product-set-generated-note" > 已 生 成 { selectedProductSetOutput . label } 预 览 < / p > : null }
< section className = "product-set-floating-detail" aria-label = "信息详情" >
< div className = "product-set-floating-detail__head" >
< strong > 信 息 详 情 < / strong >
< span > { productSetRequirement . length } / 500 < / span >
< / div >
< textarea
value = { productSetRequirement }
onChange = { ( event ) = > setProductSetRequirement ( event . target . value ) }
maxLength = { 500 }
placeholder = "建议包含以下信息:产品名称、核心卖点、期望场景、具体参数"
/ >
< button type = "button" className = "product-set-floating-submit" disabled = { ! canGenerateSet } onClick = { handleSetGenerate } >
{ productSetStatus === "generating" ? < LoadingOutlined / > : null }
{ setPrimaryLabel }
< / button >
< / section >
< button type = "button" className = "product-clone-help" aria-label = "帮助" >
< QuestionCircleOutlined / >
< / button >
< / main >
) ;
const clonePreview = (
< main className = "product-clone-preview clone-ai-preview" aria-label = "电商AI作图预览" >
< header className = "clone-ai-preview-header" >
< strong > 预 览 < / strong >
< span >
上 传 商 品 图 , AI 即 刻 生 成 < b > 符 合 多 电 商 平 台 规 范 < / b > 的 高 转 化 率 商 品 素 材 。
< / span >
2026-06-03 16:42:57 +08:00
< div className = "clone-ai-preview-summary" aria-label = "当前生成配置" >
< span > { selectedCloneOutput . label } < / span >
< span > { platform } < / span >
< span > { market } < / span >
< span > { language } < / span >
< span > { formatRatioDisplayValue ( ratio ) } < / span >
< / div >
2026-06-02 12:38:01 +08:00
< / header >
{ status === "done" ? (
< section className = "clone-ai-preview-showcase" aria-label = "生成结果" >
2026-06-02 22:09:12 +08:00
< button type = "button" className = "clone-ai-main-result" onClick = { ( ) = > openProductSetPreview ( cloneOutput === "set" ? clonePreviewCards [ 0 ] : results [ 0 ] ) } >
< img src = { productImages [ 0 ] ? . src ? ? ( cloneOutput === "set" ? clonePreviewCards [ 0 ] . src : results [ 0 ] ? . src ? ? "" ) } alt = "上传商品原图" / >
2026-06-02 12:38:01 +08:00
< span > 原 图 素 材 < / span >
< / button >
< div className = "clone-ai-flow-arrow" aria-hidden = "true" / >
2026-06-02 17:37:51 +08:00
< div className = "clone-ai-result-grid result-reveal" >
2026-06-02 22:09:12 +08:00
{ cloneOutput === "set" ? (
clonePreviewCards . map ( ( card ) = > (
< button key = { card . id } type = "button" onClick = { ( ) = > openProductSetPreview ( card ) } >
< img src = { card . src } alt = { card . label } / >
< span > { card . label } < / span >
< / button >
) )
) : results [ 0 ] ? . src ? (
< button type = "button" onClick = { ( ) = > openProductSetPreview ( results [ 0 ] ) } >
< img src = { results [ 0 ] . src } alt = { selectedCloneOutput . label } / >
< span > { selectedCloneOutput . label } < / span >
2026-06-02 12:38:01 +08:00
< / button >
2026-06-02 22:09:12 +08:00
) : null }
2026-06-02 12:38:01 +08:00
< / div >
< / section >
) : (
< section className = "clone-ai-empty-state" aria-live = "polite" >
2026-06-03 20:19:07 +08:00
{ status === "generating" ? < LoadingOutlined / > : status === "failed" ? < FrownOutlined / > : < FileImageOutlined / > }
< strong > { status === "generating" ? "正在生成" : status === "failed" ? "生成失败" : "等待生成" } < / strong >
2026-06-02 17:37:51 +08:00
{ status === "generating" ? < EcommerceProgressBar status = "generating" label = { ` ${ selectedCloneOutput . label } 生成 ` } / > : null }
2026-06-02 12:38:01 +08:00
< span >
{ status === "generating"
? ` AI 正在为 ${ platform } / ${ market } 整理 ${ selectedCloneOutput . label } 。 `
2026-06-03 20:19:07 +08:00
: status === "failed"
? "请检查网络后点击下方重试"
2026-06-02 12:38:01 +08:00
: "上传商品原图并填写信息后,AI 将在这里展示生成结果。" }
< / span >
2026-06-03 20:19:07 +08:00
{ status === "failed" && lastFailedActionRef . current ? (
< button type = "button" className = "clone-ai-retry-btn" onClick = { lastFailedActionRef . current } >
< ReloadOutlined / > 重 试
< / button >
) : null }
2026-06-02 12:38:01 +08:00
< / section >
) }
< section className = "clone-ai-bottom-input" aria-label = "信息详情" >
< div className = "clone-ai-input-wrapper" >
< textarea
ref = { requirementTextareaRef }
value = { requirement }
onChange = { ( event ) = > {
const nextValue = event . target . value . slice ( 0 , 500 ) ;
setRequirement ( nextValue ) ;
syncRequirementMentionQuery ( nextValue , event . target . selectionStart ) ;
} }
onClick = { ( event ) = > syncRequirementMentionQuery ( requirement , event . currentTarget . selectionStart ) }
onKeyUp = { ( event ) = > syncRequirementMentionQuery ( requirement , event . currentTarget . selectionStart ) }
onKeyDown = { ( event ) = > {
if ( event . key === "Escape" ) setRequirementImageMentionQuery ( null ) ;
} }
maxLength = { 500 }
placeholder = "建议包含以下信息,产品名称,核心卖点,期望场景,具体参数"
/ >
{ requirementImageMentionQuery !== null && ecommerceMentionImages . length ? (
< ImageMentionMenu images = { ecommerceMentionImages } query = { requirementImageMentionQuery } onSelect = { insertRequirementImageMention } / >
) : null }
< button type = "button" className = "clone-ai-send-button" disabled = { ! canGenerate } onClick = { handleGenerate } aria-label = { clonePrimaryLabel } >
{ status === "generating" ? < LoadingOutlined / > : "↑" }
< / button >
< / div >
< span className = "clone-ai-char-count" > { requirement . length } / 500 < / span >
< / section >
< / main >
) ;
const detailPreview = (
< main className = "product-clone-preview product-clone-preview--detail" aria-label = "A+详情预览" >
< div className = "product-clone-preview__headline" >
< h1 > A + / 详 情 页 < / h 1 >
< p >
上 传 商 品 图 , AI 即 刻 生 成 < span > 符 合 多 电 商 平 台 规 范 < / span > 的 专 业 详 情 页 。
< / p >
< / div >
< section className = "product-detail-demo-board" >
< div className = "product-detail-source-stack" >
{ ( detailProductImages . length ? detailProductImages . map ( ( item ) = > item . src ) : detailProductSamples ) . map ( ( src , index ) = > (
< figure key = { ` ${ src } - ${ index } ` } >
< img src = { src } alt = { ` 商品原图 ${ index + 1 } ` } / >
< / figure >
) ) }
< span > 上 传 产 品 图 < / span >
< / div >
< div className = "product-detail-flow-arrow" aria-hidden = "true" / >
< div className = "product-detail-long-result" >
2026-06-02 21:31:43 +08:00
< img src = { detailResultUrl ? ? detailAssets . longPage } alt = "生成电商长图" / >
2026-06-02 12:38:01 +08:00
< span > { detailStatus === "done" ? "已生成电商长图" : "生成电商长图" } < / span >
< / div >
< div className = "product-detail-grid-result" >
{ detailGridSamples . map ( ( src , index ) = > (
< img key = { src } src = { src } alt = { ` 详情页模块 ${ index + 1 } ` } / >
) ) }
< span > 符 合 多 电 商 平 台 规 范 < / span >
< / div >
< / section >
< button type = "button" className = "product-clone-help" aria-label = "帮助" >
< QuestionCircleOutlined / >
< / button >
< / main >
) ;
const tryOnPreview = (
< main className = "product-clone-preview product-clone-preview--try-on" aria-label = "服饰穿戴预览" >
< div className = "product-clone-preview__headline" >
< h1 > AI服饰穿戴 < / h1 >
< p > 上 传 服 装 图 , 定 制 专 属 模 特 , 即 刻 生 成 多 种 场 景 不 同 姿 势 套 图 。 < / p >
< / div >
{ tryOnResultImages . length ? (
< section className = "product-try-on-generated" aria-label = "生成结果" >
{ tryOnResultImages . map ( ( src , index ) = > (
< figure key = { src } >
< img src = { src } alt = { ` 生成结果 ${ index + 1 } ` } / >
< figcaption > { selectedScenes [ index % Math . max ( selectedScenes . length , 1 ) ] || "智能场景" } < / figcaption >
< / figure >
) ) }
< / section >
) : null }
< section className = "product-try-on-demo-board" >
{ tryOnCards . map ( ( card ) = > (
< article key = { card . title } className = { ` product-try-on-card product-try-on-card-- ${ card . tone } ` } >
< h2 > { card . title } < / h2 >
< div className = "product-try-on-inputs" >
{ card . inputs . map ( ( src , index ) = > (
< div className = "product-try-on-input-group" key = { ` ${ card . title } - ${ src } ` } >
< img src = { src } alt = { ` ${ card . title } 输入 ${ index + 1 } ` } / >
{ index < card . inputs . length - 1 ? < span className = "product-try-on-plus" > + < / span > : null }
< / div >
) ) }
< / div >
< div className = "product-try-on-arrow" aria-hidden = "true" / >
< div className = "product-try-on-results" >
{ card . results . map ( ( src , index ) = > (
< img key = { src } src = { src } alt = { ` ${ card . title } 示例 ${ index + 1 } ` } / >
) ) }
< / div >
< / article >
) ) }
< / section >
< button type = "button" className = "product-clone-help" aria-label = "帮助" >
< QuestionCircleOutlined / >
< / button >
< / main >
) ;
const placeholderPreview = (
< main className = "product-clone-preview product-clone-preview--placeholder" aria-label = { ` ${ pageLabel } 预览 ` } >
< div className = "product-clone-preview__headline" >
< h1 > { pageLabel } < / h1 >
< p > 选 择 左 侧 已 接 入 的 工 具 开 始 生 成 商 品 视 觉 内 容 。 < / p >
< / div >
< button type = "button" className = "product-clone-help" aria-label = "帮助" >
< QuestionCircleOutlined / >
< / button >
< / main >
) ;
return (
< section
className = { ` product-clone-page page-motion ${ isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : "" } ` }
data-tool = { activeTool }
aria-label = { pageLabel }
>
< div className = "product-clone-shell" >
< aside className = "product-clone-rail" aria-label = "商品工具" >
{ sideTools . map ( ( tool ) = > (
< button key = { tool . key } type = "button" className = { activeTool === tool . key ? "is-active" : "" } onClick = { ( ) = > setActiveTool ( tool . key ) } >
{ tool . icon }
< span > { tool . label } < / span >
< / button >
) ) }
< / aside >
< aside
id = { isCloneTool ? "ecommerce-clone-settings-panel" : undefined }
2026-06-02 18:31:39 +08:00
className = { ` product-clone-panel tool-panel-enter ` }
key = { activeTool }
2026-06-02 12:38:01 +08:00
aria-label = { ` ${ pageLabel } 参数 ` }
aria-hidden = { isCloneTool && isCloneSettingsCollapsed ? true : undefined }
>
{ isSetTool ? setPanel : isDetail ? detailPanel : isTryOn ? tryOnPanel : isCloneTool ? clonePanel : placeholderPanel }
< / aside >
{ isCloneTool ? (
< button
type = "button"
className = "clone-ai-settings-toggle"
onClick = { ( ) = > setIsCloneSettingsCollapsed ( ( current ) = > ! current ) }
aria-label = { isCloneSettingsCollapsed ? "展开设置面板" : "收起设置面板" }
aria-controls = "ecommerce-clone-settings-panel"
aria-expanded = { ! isCloneSettingsCollapsed }
title = { isCloneSettingsCollapsed ? "展开设置面板" : "收起设置面板" }
>
{ isCloneSettingsCollapsed ? < MenuUnfoldOutlined / > : < MenuFoldOutlined / > }
< / button >
) : null }
{ isSetTool ? setPreview : isDetail ? detailPreview : isTryOn ? tryOnPreview : isCloneTool ? ( cloneOutput === "video" ? (
< main className = "product-clone-preview product-clone-preview--video" style = { { padding : 0 , overflow : "hidden" } } >
< EcommerceVideoWorkspace
isAuthenticated = { Boolean ( ( _props as Record < string , unknown > ) . isAuthenticated ) }
productImageDataUrls = { productImages . map ( ( img ) = > img . src ) }
requirement = { requirement }
platform = { platform }
aspectRatio = { ratio . includes ( "9: 16" ) || ratio . includes ( "9:16" ) ? "9:16" : ratio . includes ( "16: 9" ) || ratio . includes ( "16:9" ) ? "16:9" : ratio . includes ( "3: 4" ) || ratio . includes ( "3:4" ) ? "3:4" : "9:16" }
durationSeconds = { cloneVideoDuration }
resolution = { cloneVideoQuality === "standard" ? "720P" : "1080P" }
2026-06-03 01:39:06 +08:00
onRequestLogin = { ( ) = > ( ( _props as Record < string , unknown > ) . isAuthenticated ? undefined : ( window . location . hash = "#/login" ) ) }
2026-06-02 12:38:01 +08:00
/ >
< / main >
2026-06-03 20:19:07 +08:00
) : cloneOutput === "video-outfit" && results . length > 0 && results [ 0 ] . type === "video" ? (
< main className = "product-clone-preview product-clone-preview--video-outfit" style = { { display : "flex" , alignItems : "center" , justifyContent : "center" } } >
< div style = { { maxWidth : "100%" , maxHeight : "100%" } } >
< video src = { results [ 0 ] . src } controls style = { { maxWidth : "100%" , maxHeight : "70vh" , borderRadius : "12px" } } / >
< / div >
< / main >
2026-06-02 12:38:01 +08:00
) : clonePreview ) : placeholderPreview }
< / div >
{ selectedProductSetPreview ? (
< div className = "product-set-preview-backdrop" role = "presentation" onClick = { ( ) = > setSelectedProductSetPreview ( null ) } >
< section
className = "product-set-preview-modal"
role = "dialog"
aria-modal = "true"
aria-label = { selectedProductSetPreview . label }
onClick = { ( event ) = > event . stopPropagation ( ) }
>
< button
type = "button"
className = "product-set-preview-close"
onClick = { ( ) = > setSelectedProductSetPreview ( null ) }
aria-label = "关闭预览"
>
< CloseOutlined / >
< / button >
< img src = { selectedProductSetPreview . src } alt = { selectedProductSetPreview . label } / >
< strong > { selectedProductSetPreview . label } < / strong >
< / section >
< / div >
) : null }
{ showHostingModal ? (
< div className = "product-set-hosting-backdrop" role = "presentation" >
< section className = "product-set-hosting-modal" role = "dialog" aria-modal = "true" aria-label = "批量托管上线啦" >
< img src = { productSetAssets . hosting } alt = "托管模式" / >
< div className = "product-set-hosting-content" >
< button type = "button" className = "product-set-hosting-close" onClick = { ( ) = > setShowHostingModal ( false ) } aria-label = "关闭" >
×
< / button >
< h2 >
批 量 托 管 上 线 啦 !
< span > 批 量 6 折 < / span >
< / h2 >
< strong > 睡 一 觉 , 图 就 做 好 了 ! < / strong >
< ul >
< li >
< b > 批 量 生 产 < / b >
< span > 支 持 无 限 任 务 并 行 生 成 , 效 率 直 线 飞 升 。 < / span >
< / li >
< li >
< b > 成 本 立 省 40 % < / b >
< span > 调 度 夜 间 闲 置 算 力 , 享 受 专 属 离 线 点 数 折 扣 。 < / span >
< / li >
< li >
< b > AI智能提取 < / b >
< span > 自 动 识 别 图 片 卖 点 , 生 成 高 转 化 销 售 卖 点 。 < / span >
< / li >
< / ul >
< button type = "button" className = "product-set-hosting-confirm" onClick = { ( ) = > setShowHostingModal ( false ) } >
我 知 道 了
< / button >
< / div >
< / section >
< / div >
) : null }
< / section >
) ;
}
export default ProductClonePage ;