From ec9204437dbbf1e8b5f8ad2a71342d83ae104134 Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Tue, 2 Jun 2026 22:09:12 +0800 Subject: [PATCH] feat(ecommerce): dynamic set image count + per-type API calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously all 4 image tools generated a single image and duplicated it across 5 fixed card slots. Now: - Set (套图) mode: uses cloneSetCounts (卖点图/白底图/场景图 quantity) to determine how many images to generate. Each type gets its own createImageTask call with a type-specific prompt, so images differ by category (selling-point vs white-background vs lifestyle scene). - Preview cards are dynamically built from cloneSetCounts, not from the fixed 5-slot productSetPreviewCards template. A card labeled "卖点图 1", "卖点图 2" etc for count > 1, or just "卖点图" for count = 1. - clonePreview: shows dynamic card grid for set mode, single result for detail/model/hot modes. - setPreview: shows original image as main card, then all generated set cards in the grid. - generateSetImages: new async function that loops over each count type and generates images sequentially with distinct prompts. Co-Authored-By: Claude Opus 4.7 --- src/features/ecommerce/EcommercePage.tsx | 203 +++++++++++++++++------ 1 file changed, 152 insertions(+), 51 deletions(-) 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 = {}) {