diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index c3854f0..a193e2b 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -1364,21 +1364,35 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { 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): string => { const parts: string[] = []; - if (outputKey === "set") { - parts.push("Generate a complete set of e-commerce product images for online marketplace listing."); - parts.push(`Include: main product image with clean white background, lifestyle/scene image showing product in real use, model-wearing image if applicable, detail/close-up image highlighting texture and quality, selling-point infographic image.`); + 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("All images must comply with platform image guidelines — proper margins, no watermark, no prohibited content."); - } else if (outputKey === "detail") { - parts.push("Generate a series of A+ detail page images for an e-commerce product listing."); - parts.push(`Create images for each section: hero/first-screen visual, core selling points, usage scenes, multi-angle views, product detail close-ups.`); - parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`); - parts.push("Design should follow platform A+ page best practices — clear hierarchy, professional typography, high visual impact."); + 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("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}.`); parts.push("Model should appear natural and appealing. Background should complement the product. Image must meet platform standards."); } else if (outputKey === "hot") { @@ -1393,6 +1407,67 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { 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 resultUrl = await waitForTask(taskId, { + abortRef: imageAbortRef.current, + onProgress: () => {}, + }); + + if (resultUrl) { + generatedUrls.push(resultUrl); + } else { + generatedUrls.push(""); + } + } + } + + setResultFn(generatedUrls); + setStatusFn(generatedUrls.some(Boolean) ? "done" : "idle"); + } catch (err) { + if (err instanceof ServerRequestError && err.status === 402) { + setResultFn([]); + } + setStatusFn("idle"); + } + }; + const generateEcommerceImage = async ( outputKey: CloneOutputKey, images: CloneImageItem[], @@ -1413,7 +1488,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { } const prompt = buildEcommerceImagePrompt(outputKey, userText, pPlatform, pRatio, pLanguage, pMarket); - const cardLabels = productSetPreviewCards.map((c) => c.label); const stamp = Date.now(); const { taskId } = await aiGenerationClient.createImageTask({ @@ -1421,7 +1495,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { prompt, ratio: pRatio, quality: pRatio.includes("720") ? "720P" : "1080P", - gridMode: outputKey === "set" ? "grid" : "single", + gridMode: "single", referenceUrls, }); @@ -1431,24 +1505,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { }); if (resultUrl) { - setResultFn( - cardLabels.map((label, i) => ({ - id: `ecommerce-${stamp}-${i}`, - src: resultUrl, - label, - })), - ); + setResultFn([{ id: `ecommerce-${stamp}`, src: resultUrl, label: selectedCloneOutput.label }]); setStatusFn("done"); } else { setStatusFn("idle"); } } catch (err) { if (err instanceof ServerRequestError && err.status === 402) { - setResultFn([{ - id: `ecommerce-error-402`, - src: "", - label: "余额不足,请充值后继续", - }]); + setResultFn([{ id: `ecommerce-error-402`, src: "", label: "余额不足,请充值后继续" }]); } setStatusFn("idle"); } @@ -1656,11 +1720,20 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const handleGenerate = () => { if (!canGenerate) return; imageAbortRef.current = { current: false }; - void generateEcommerceImage( - cloneOutput, productImages, requirement, - platform, ratio, language, market, - (s) => setStatus(s as ProductCloneStatus), setResults, - ); + 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, + ); + } }; const handleGenerateModel = () => { @@ -1703,11 +1776,11 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const handleSetGenerate = () => { if (!canGenerateSet) return; imageAbortRef.current = { current: false }; - void generateEcommerceImage( - "set", setImages, productSetRequirement, + void generateSetImages( + setImages, cloneSetCounts, productSetRequirement, productSetPlatform, productSetRatio, productSetLanguage, productSetMarket, (s) => setProductSetStatus(s as ProductSetStatus), - (res) => setProductSetResultImages(res.map((r) => r.src).filter(Boolean)), + (urls) => setProductSetResultImages(urls), ); }; @@ -1797,14 +1870,35 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { detailProductImages.length === 0 ? "请上传产品图" : detailStatus === "generating" ? "生成中..." : "生成A+详情页"; const clonePrimaryLabel = productImages.length === 0 ? "请先上传商品原图" : status === "generating" ? "生成中..." : `生成${selectedCloneOutput.label}`; - const setPreviewCards = productSetPreviewCards.map((card, index) => ({ - ...card, - src: productSetResultImages[index] ?? card.src, - })); - const clonePreviewCards = productSetPreviewCards.map((card, index) => ({ - ...card, - src: results[index]?.src ?? card.src, - })); + 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; @@ -2723,14 +2817,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {