import { AppstoreOutlined, CloudUploadOutlined, CloseOutlined, FileImageOutlined, FrownOutlined, LoadingOutlined, MenuFoldOutlined, MenuUnfoldOutlined, QuestionCircleOutlined, ReloadOutlined, SettingOutlined, SkinOutlined, } from "@ant-design/icons"; import { useEffect, useRef, useState, type CSSProperties, type ChangeEvent, type DragEvent, type ReactNode } from "react"; import { EcommerceProgressBar } from "./EcommerceProgressBar"; 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`; import ImageMentionMenu, { getImageMentionQuery, insertImageMentionValue, type MentionImageOption } from "./ImageMentionMenu"; import EcommerceVideoWorkspace from "./EcommerceVideoWorkspace"; import EcommerceDetailPanel from "./panels/EcommerceDetailPanel"; import EcommerceSetPanel from "./panels/EcommerceSetPanel"; import EcommerceTryOnPanel from "./panels/EcommerceTryOnPanel"; import EcommerceClonePanel from "./panels/EcommerceClonePanel"; import { aiGenerationClient } from "../../api/aiGenerationClient"; import { ServerRequestError } from "../../api/serverConnection"; import { waitForTask } from "../../api/taskSubscription"; import { toast } from "../../components/toast/toastStore"; import { useGenerationTasks } from "../../hooks/useGenerationTasks"; import { useAppStore } from "../../stores"; import { normalizeEcommerceImageMime, summarizeRejectedImages, validateEcommerceImageFiles, } from "./ecommerceImageValidation"; interface ProductClonePageProps { [key: string]: unknown; } type ProductCloneStatus = "idle" | "ready" | "generating" | "done" | "failed"; type ProductSetOutputKey = "set" | "detail" | "model" | "video"; type CloneOutputKey = ProductSetOutputKey | "hot" | "video-outfit"; type CloneSetCountKey = "selling" | "white" | "scene"; type CloneModelPanelTab = "scene" | "model"; type CloneVideoQualityKey = "standard" | "high" | "ultra"; type ProductSetStatus = "idle" | "ready" | "generating" | "done" | "failed"; 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"; type TryOnStatus = "idle" | "modeling" | "ready" | "generating" | "done" | "failed"; type DetailStatus = "idle" | "ready" | "generating" | "done" | "failed"; 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; 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; } type PlatformRatioModeKey = ProductSetOutputKey | "hot" | "video-outfit"; interface PlatformRatioGroup { ratios: string[]; defaultRatio: string; } const sideTools: Array<{ key: ProductKitToolKey; label: string; icon: ReactNode }> = [ { key: "set", label: "商品套图", icon: }, { key: "detail", label: "A+详情", icon: }, { key: "wear", label: "服饰穿戴", icon: }, { key: "clone", label: "电商AI作图", icon: }, ]; const platformSpecOptions: Array<{ label: string; ratios: string[]; defaultRatio: string; ratioGroups?: Partial>; 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 = { 英语: "英文", 日语: "日文", 德语: "德文", 法语: "法文", 韩语: "韩文", 西文: "西班牙语", 葡文: "葡萄牙语", 印尼语: "印度尼西亚语", 菲律宾语: "菲律宾语(他加禄语)", }; 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*px?/u)?.[0]?.replace(/\s+/g, "") ?? ""; const ratio = value.match(/\d+(?:\.\d+)?\s*[::]\s*\d+(?:\.\d+)?/u)?.[0]?.replace(/\s+/g, "").replace(/:/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: "爆款图复刻" }, { key: "video-outfit", label: "视频换装" }, ]; const cloneSetCountOptions: Array<{ key: CloneSetCountKey; title: string; desc: string; }> = [ { key: "selling", title: "卖点图", desc: "展示商品的核心卖点及细节特写" }, { key: "white", title: "白底图", desc: "白底主图,多角度呈现商品细节" }, { key: "scene", title: "场景图", desc: "展示商品的生活使用场景和人物搭配" }, ]; const defaultCloneSetCounts: Record = { 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((file, index) => ({ id: `${prefix}-${Date.now()}-${index}`, src: URL.createObjectURL(file), name: file.name, format: getImageFileFormat(file), })); } function notifyRejectedImages(files: File[]): File[] { const { accepted, rejected } = validateEcommerceImageFiles(files); const message = summarizeRejectedImages(rejected); if (message) toast.error(message); return accepted; } function isCloneSavedSetting(item: unknown): item is CloneSavedSetting { const candidate = item as Partial; 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(null); const productInputRef = useRef(null); const cloneReferenceInputRef = useRef(null); const requirementTextareaRef = useRef(null); const garmentInputRef = useRef(null); const detailInputRef = useRef(null); const countHoldTimeoutRef = useRef(null); const countHoldIntervalRef = useRef(null); const imageGen = useGenerationTasks({ sourceView: "ecommerce" }); const appUsage = useAppStore((s) => s.usage); const latestCloneSettingRef = useRef(null); const skipInitialCloneAutoSaveRef = useRef(true); const skipNextCloneAutoSaveRef = useRef(false); const [activeTool, setActiveTool] = useState("clone"); const [setImages, setSetImages] = useState([]); 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("video"); const [productSetStatus, setProductSetStatus] = useState("idle"); const [productSetResultImages, setProductSetResultImages] = useState([]); const [isSetUploadDragging, setIsSetUploadDragging] = useState(false); const [selectedProductSetPreview, setSelectedProductSetPreview] = useState<{ src: string; label: string } | null>(null); const [showHostingModal, setShowHostingModal] = useState(false); const [productImages, setProductImages] = useState([]); const [isProductUploadDragging, setIsProductUploadDragging] = useState(false); const [cloneOutput, setCloneOutput] = useState("detail"); const [openCloneBasicSelect, setOpenCloneBasicSelect] = useState(null); const [openCloneModelSelect, setOpenCloneModelSelect] = useState(null); const [cloneModelSelectDropUp, setCloneModelSelectDropUp] = useState(false); const [cloneReferenceMode, setCloneReferenceMode] = useState("upload"); const [cloneReferenceImages, setCloneReferenceImages] = useState([]); const [cloneReplicateLevel, setCloneReplicateLevel] = useState("high"); const [cloneSetCounts, setCloneSetCounts] = useState(defaultCloneSetCounts); const [selectedCloneDetailModules, setSelectedCloneDetailModules] = useState(defaultCloneDetailModuleIds); const [cloneModelPanelTab, setCloneModelPanelTab] = useState("scene"); const [selectedCloneModelScenes, setSelectedCloneModelScenes] = useState([]); 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("high"); const [cloneVideoDuration, setCloneVideoDuration] = useState(10); const [cloneVideoSmart, setCloneVideoSmart] = useState(true); const [videoOutfitVideoFile, setVideoOutfitVideoFile] = useState(null); const [videoOutfitRefFile, setVideoOutfitRefFile] = useState(null); const [isCloneSettingsCollapsed, setIsCloneSettingsCollapsed] = useState(false); const [requirement, setRequirement] = useState(""); const [requirementImageMentionQuery, setRequirementImageMentionQuery] = useState(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("idle"); const [results, setResults] = useState([]); const imageAbortRef = useRef({ current: false }); const lastFailedActionRef = useRef<(() => void) | null>(null); const [garmentImages, setGarmentImages] = useState([]); const [modelSource, setModelSource] = useState("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([]); const [customScene, setCustomScene] = useState(""); const [smartScene, setSmartScene] = useState(false); const [tryOnRatio, setTryOnRatio] = useState(tryOnRatioOptions[0]); const [tryOnStatus, setTryOnStatus] = useState("idle"); const [tryOnResultImages, setTryOnResultImages] = useState([]); const [detailProductImages, setDetailProductImages] = useState([]); 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(defaultDetailModuleIds); const [detailStatus, setDetailStatus] = useState("idle"); const [detailResultUrl, setDetailResultUrl] = useState(null); 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"; const canGenerate = (cloneOutput === "video-outfit" ? videoOutfitVideoFile && videoOutfitRefFile : productImages.length > 0) && status !== "generating"; 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; const imageFiles = notifyRejectedImages(files); 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) => { const files = event.target.files; if (!files?.length) return; addSetImages(Array.from(files)); event.target.value = ""; }; const handleSetDrop = (event: DragEvent) => { 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[]) => { const imageFiles = notifyRejectedImages(files); 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) => { const files = event.target.files; if (!files?.length) return; addProductImages(Array.from(files)); event.target.value = ""; }; const handleProductDrop = (event: DragEvent) => { 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[]) => { const imageFiles = notifyRejectedImages(files); 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) => { 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) => { const files = event.target.files; if (!files?.length) return; const uploadedFiles = notifyRejectedImages(Array.from(files)); if (!uploadedFiles.length) { event.target.value = ""; return; } setGarmentImages((current) => [...current, ...createObjectImageItems(uploadedFiles, 5 - current.length, "garment")].slice(0, 5)); setTryOnStatus("ready"); event.target.value = ""; }; const handleDetailUpload = (event: ChangeEvent) => { const files = event.target.files; if (!files?.length) return; const uploadedFiles = notifyRejectedImages(Array.from(files)); if (!uploadedFiles.length) { event.target.value = ""; return; } setDetailProductImages((current) => [...current, ...createObjectImageItems(uploadedFiles, 3 - current.length, "detail")].slice(0, 3)); setDetailStatus("ready"); event.target.value = ""; }; const blobToDataUrl = (blob: Blob): Promise => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || "")); reader.onerror = () => reject(reader.error || new Error("文件读取失败")); reader.readAsDataURL(blob); }); const uploadCloneImages = async (images: CloneImageItem[]): Promise => { const urls: string[] = []; for (const item of images) { try { const resp = await fetch(item.src); const rawBlob = await resp.blob(); const mimeType = normalizeEcommerceImageMime(rawBlob.type); const blob = rawBlob.type === mimeType ? rawBlob : new Blob([rawBlob], { type: mimeType }); const dataUrl = await blobToDataUrl(blob); const { url } = await aiGenerationClient.uploadAsset({ dataUrl, name: item.name, mimeType, scope: "ecommerce-product" }); urls.push(url); } catch { // skip images that fail to upload } } return urls; }; const IMAGE_MODEL = "gpt-image-2"; const setCountLabels: Record = { 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(" "); }; 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 => { const parts: string[] = []; 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."); parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`); parts.push("Follow platform A+ page best practices — clear hierarchy, professional typography, high visual impact."); } else if (outputKey === "model") { parts.push("Generate model/try-on lifestyle images for an e-commerce product listing."); parts.push("Show the product being used or worn by a model in attractive lifestyle settings."); parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`); 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."); } 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(" "); }; const generateSetImages = async ( images: CloneImageItem[], counts: Record, userText: string, pPlatform: string, pRatio: string, pLanguage: string, pMarket: string, setStatusFn: (status: "generating" | "done" | "idle") => void, setResultFn: (urls: string[]) => void, ): Promise => { 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, }); const storeId = imageGen.submitTask({ title: `${setCountLabels[countKey].label} ${i + 1}`, type: "image", status: "running", progress: 5, prompt: fullPrompt, sourceView: "ecommerce", taskId }); const resultUrl = await waitForTask(taskId, { abortRef: imageAbortRef.current, onProgress: () => {}, }); if (resultUrl) { generatedUrls.push(resultUrl); imageGen.updateTask(storeId, { status: "completed", progress: 100, resultUrl }); } else { generatedUrls.push(""); imageGen.updateTask(storeId, { status: "failed", error: "生成未返回结果" }); } } } setResultFn(generatedUrls); setStatusFn(generatedUrls.some(Boolean) ? "done" : "idle"); } catch (err) { if (err instanceof ServerRequestError && err.status === 402) { setResultFn([]); toast.error("余额不足,请充值后继续"); } else { const msg = err instanceof Error ? err.message : "生成失败"; toast.error(msg); } setStatusFn("failed"); } }; const generateEcommerceImage = async ( outputKey: CloneOutputKey, images: CloneImageItem[], 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 }, statusFn?: (status: "generating" | "done" | "idle" | "failed") => void, resultFn?: (results: CloneImageItem[]) => void, ): Promise => { setStatusFn("generating"); try { const referenceUrls = await uploadCloneImages(images); if (!referenceUrls.length) { setStatusFn("idle"); return; } const prompt = buildEcommerceImagePrompt(outputKey, userText, pPlatform, pRatio, pLanguage, pMarket, tryOnOptions); const stamp = Date.now(); const { taskId } = await aiGenerationClient.createImageTask({ model: IMAGE_MODEL, prompt, ratio: pRatio, quality: pRatio.includes("720") ? "720P" : "1080P", gridMode: "single", referenceUrls, }); const storeId = imageGen.submitTask({ title: `电商${outputKey}图`, type: "image", status: "running", progress: 5, prompt, sourceView: "ecommerce", taskId }); const resultUrl = await waitForTask(taskId, { abortRef: imageAbortRef.current, onProgress: () => {}, }); if (resultUrl) { setResultFn([{ id: `ecommerce-${stamp}`, src: resultUrl, label: selectedCloneOutput.label }]); setStatusFn("done"); imageGen.updateTask(storeId, { status: "completed", progress: 100, resultUrl }); } else { setStatusFn("idle"); imageGen.updateTask(storeId, { status: "failed", error: "生成未返回结果" }); } } catch (err) { if (err instanceof ServerRequestError && err.status === 402) { setResultFn([{ id: `ecommerce-error-402`, src: "", label: "余额不足,请充值后继续" }]); toast.error("余额不足,请充值后继续"); } else { const msg = err instanceof Error ? err.message : "生成失败"; toast.error(msg); } setStatusFn("failed"); } }; const handleVideoOutfitGenerate = async () => { if (!videoOutfitVideoFile || !videoOutfitRefFile) return; setStatus("generating"); try { const readAsDataUrl = (file: File): Promise => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.onerror = () => reject(new Error("文件读取失败")); reader.readAsDataURL(file); }); const videoDataUrl = await readAsDataUrl(videoOutfitVideoFile); const refDataUrl = await readAsDataUrl(videoOutfitRefFile); 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", }); const { taskId } = await aiGenerationClient.createVideoEditTask({ videoUrl: videoAsset.url, referenceUrls: [refAsset.url], prompt: requirement || undefined, }); 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"); } catch (err) { setStatus("failed"); toast.error(err instanceof Error ? err.message : "视频换装生成失败"); } }; const handleGenerate = () => { if (!canGenerate) return; if ((appUsage?.balanceCents ?? 0) <= 0) { toast.error("积分不足,请充值后继续"); return; } if (cloneOutput === "set" && cloneSetTotal > 5) { if (!window.confirm(`将生成 ${cloneSetTotal} 张图片,可能消耗较多积分,是否继续?`)) return; } imageAbortRef.current = { current: false }; lastFailedActionRef.current = null; if (cloneOutput === "video-outfit") { void handleVideoOutfitGenerate(); } else if (cloneOutput === "set") { 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, ); lastFailedActionRef.current = () => handleGenerate(); } }; const handleGenerateModel = () => { imageAbortRef.current = { current: false }; lastFailedActionRef.current = null; setTryOnStatus("modeling"); void generateEcommerceImage( "model", garmentImages, requirement, platform, ratio, language, market, { gender: modelGender, age: modelAge, ethnicity: modelEthnicity, body: modelBody, appearance, scenes: selectedScenes, smartScene }, (s) => { if (s === "done") setTryOnStatus("ready"); else setTryOnStatus(s as TryOnStatus); }, () => { setTryOnStatus("ready"); }, ); lastFailedActionRef.current = () => handleGenerateModel(); }; const handleTryOnGenerate = () => { if (!canGenerateTryOn) return; imageAbortRef.current = { current: false }; lastFailedActionRef.current = null; void generateEcommerceImage( "model", garmentImages, requirement, platform, ratio, language, market, { gender: modelGender, age: modelAge, ethnicity: modelEthnicity, body: modelBody, appearance, scenes: selectedScenes, smartScene }, (s) => setTryOnStatus(s as TryOnStatus), (res) => setTryOnResultImages(res.map((r) => r.src).filter(Boolean)), ); lastFailedActionRef.current = () => handleTryOnGenerate(); }; 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; imageAbortRef.current = { current: false }; lastFailedActionRef.current = null; void generateSetImages( setImages, cloneSetCounts, productSetRequirement, productSetPlatform, productSetRatio, productSetLanguage, productSetMarket, (s) => setProductSetStatus(s as ProductSetStatus), (urls) => setProductSetResultImages(urls), ); lastFailedActionRef.current = () => handleSetGenerate(); }; 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; imageAbortRef.current = { current: false }; lastFailedActionRef.current = null; void generateEcommerceImage( "detail", detailProductImages, detailRequirement, detailPlatform, getPlatformDefaultRatio(detailPlatform), detailLanguage, detailMarket, (s) => setDetailStatus(s as DetailStatus), (res) => setDetailResultUrl(res[0]?.src ?? null), ); }; 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}`; 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++; } } 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 = ( ); const clonePanel = ( { setVideoOutfitVideoFile(video); setVideoOutfitRefFile(ref); }} /> ); const detailPanel = ( ); const tryOnPanel = ( ); const placeholderPanel = ( <>
{activeToolMeta?.icon}

{activeToolMeta?.label}

该工具页面正在接入,当前可使用电商AI作图、商品套图、A+详情与服饰穿戴。

); const setPreview = (

预览

上传商品图,AI 即刻生成 符合多电商平台规范 的高转化率商品套图。

{productSetPreviewReady ? (
) : (
{productSetStatus === "generating" ? : } {productSetStatus === "generating" ? "正在生成" : "等待生成"} {productSetStatus === "generating" ? : null} {productSetStatus === "generating" ? "AI 正在整理主图、场景、细节与卖点图。" : "上传商品原图并填写信息后,AI 将为您生成专业的电商商品图"}
)} {productSetStatus === "done" ?

已生成{selectedProductSetOutput.label}预览

: null}
信息详情 {productSetRequirement.length}/500