diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index 6f9a3ab..093f1bc 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -523,6 +523,12 @@ const formatUploadedImageRatio = (image?: CloneImageItem) => { 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 formatProductImageSpec = (image?: CloneImageItem | null) => { + if (!image) return "等待上传"; + const format = image.format ? ` · ${image.format}` : ""; + if (!image.width || !image.height) return `正在识别尺寸${format}`; + return `${image.width}×${image.height}px · ${formatAspectRatio(image.width, image.height)}${format}`; +}; const defaultMarketLanguageOption = marketLanguageOptions[0]!; const normalizeMarket = (value: string) => marketLanguageOptions.some((option) => option.country === value) ? value : defaultMarketLanguageOption.country; @@ -778,6 +784,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const [selectedProductSetPreview, setSelectedProductSetPreview] = useState<{ src: string; label: string } | null>(null); const [showHostingModal, setShowHostingModal] = useState(false); const [productImages, setProductImages] = useState([]); + const [selectedProductImageId, setSelectedProductImageId] = useState(null); const [isProductUploadDragging, setIsProductUploadDragging] = useState(false); const [cloneOutput, setCloneOutput] = useState("detail"); const [openCloneBasicSelect, setOpenCloneBasicSelect] = useState(null); @@ -862,6 +869,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const selectedProductSetOutput = productSetOutputOptions.find((option) => option.key === productSetOutput) ?? productSetOutputOptions[0]!; const selectedCloneOutput = cloneOutputOptions.find((option) => option.key === cloneOutput) ?? cloneOutputOptions[1]!; + const selectedProductImage = productImages.find((image) => image.id === selectedProductImageId) ?? productImages[0] ?? null; + const selectedProductImageIndex = selectedProductImage + ? productImages.findIndex((image) => image.id === selectedProductImage.id) + : -1; + const selectedProductImageLabel = selectedProductImageIndex >= 0 ? `商品图 ${selectedProductImageIndex + 1}` : "商品图"; + const selectedProductImageSpec = formatProductImageSpec(selectedProductImage); + const isProductImageLimitReached = productImages.length >= maxCloneProductImages; const productSetPreviewReady = productSetStatus === "done"; const cloneSetTotal = Object.values(cloneSetCounts).reduce((sum, value) => sum + value, 0); const canGenerateSet = setImages.length > 0 && productSetStatus !== "generating"; @@ -890,6 +904,30 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { }); }; + useEffect(() => { + if (!productImages.length) { + if (selectedProductImageId !== null) setSelectedProductImageId(null); + return; + } + if (!selectedProductImageId || !productImages.some((image) => image.id === selectedProductImageId)) { + setSelectedProductImageId(productImages[0].id); + } + }, [productImages, selectedProductImageId]); + + useEffect(() => { + productImages + .filter((item) => !item.width || !item.height) + .forEach((item) => { + readImageDimensions(item.src) + .then(({ width, height }) => { + setProductImages((current) => + current.map((currentItem) => (currentItem.id === item.id ? { ...currentItem, width, height } : currentItem)), + ); + }) + .catch(() => undefined); + }); + }, [productImages]); + const addSetImages = (files: File[]) => { if (setImages.length >= 3) return; const imageFiles = files.filter((file) => file.type.startsWith("image/")); @@ -945,6 +983,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const handleProductDrop = (event: DragEvent) => { event.preventDefault(); setIsProductUploadDragging(false); + if (isProductImageLimitReached) return; const files = Array.from(event.dataTransfer.files); if (files.length) addProductImages(files); }; @@ -1815,6 +1854,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { setSelectedProductSetPreview(null); setShowHostingModal(false); setProductImages([]); + setSelectedProductImageId(null); setIsProductUploadDragging(false); setCloneOutput("detail"); setRatio((current) => normalizeRatioForPlatform(platform, current, "detail")); @@ -2061,18 +2101,24 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
productInputRef.current?.click()} + tabIndex={isProductImageLimitReached ? -1 : 0} + aria-disabled={isProductImageLimitReached} + className={`clone-ai-upload-zone${isProductUploadDragging ? " is-dragging" : ""}${isProductImageLimitReached ? " is-full" : ""}`} + onClick={() => { + if (isProductImageLimitReached) return; + productInputRef.current?.click(); + }} onKeyDown={(event) => { if (event.target !== event.currentTarget) return; if (event.key === "Enter" || event.key === " ") { event.preventDefault(); + if (isProductImageLimitReached) return; productInputRef.current?.click(); } }} onDragEnter={(event) => { event.preventDefault(); + if (isProductImageLimitReached) return; setIsProductUploadDragging(true); }} onDragOver={(event) => event.preventDefault()} @@ -2085,35 +2131,68 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { 拖拽或点击上传 - - 上传图片 + {isProductImageLimitReached ? ( + "已达上限" + ) : ( + <> + + 上传图片 + + )} 同一产品,最多 7 张
{productImages.length ? ( -
- {productImages.map((item) => ( -
- {item.name} - - -
- ))} +
event.stopPropagation()} aria-live="polite"> +
+ {`当前预览:${selectedProductImageLabel}`} +
+
+ + {selectedProductImageLabel} + {selectedProductImageSpec} + +
+
+ ) : null} + {productImages.length ? ( +
+
+ 已上传素材 + {productImages.length}/{maxCloneProductImages} +
+
+ {productImages.map((item, index) => ( +
+ + +
+ ))} +
) : null}
- +
diff --git a/src/features/workbench/WorkbenchSelectChips.tsx b/src/features/workbench/WorkbenchSelectChips.tsx index 593b185..6987c69 100644 --- a/src/features/workbench/WorkbenchSelectChips.tsx +++ b/src/features/workbench/WorkbenchSelectChips.tsx @@ -3,6 +3,24 @@ import type { ReactNode } from "react"; import type { WorkbenchOption, WorkbenchFieldGroup } from "./workbenchConstants"; import { getRatioOptionClassName, getSettingsGridColumnsClassName } from "./workbenchReferenceUtils"; +const VIDEO_MODEL_ICON_URLS = { + happyHorse: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/HappyHorse.svg", + pixverse: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/Pixverse.svg", + vidu: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/viduQ3.svg", + wanxiang: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/wan.svg", + kling: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/kling.svg", +} as const; + +function getVideoModelIconUrl(option: WorkbenchOption): string | null { + const text = `${option.value} ${option.label}`.toLowerCase(); + if (text.includes("happyhorse")) return VIDEO_MODEL_ICON_URLS.happyHorse; + if (text.includes("pixverse")) return VIDEO_MODEL_ICON_URLS.pixverse; + if (text.includes("vidu")) return VIDEO_MODEL_ICON_URLS.vidu; + if (text.includes("wan") || text.includes("万相")) return VIDEO_MODEL_ICON_URLS.wanxiang; + if (text.includes("kling") || text.includes("可灵")) return VIDEO_MODEL_ICON_URLS.kling; + return null; +} + export function SelectChip({ chipId, value, @@ -56,6 +74,7 @@ export function SelectChip({ > {options.map((option, index) => { const active = option.value === value; + const iconUrl = chipId === "video-model" ? getVideoModelIconUrl(option) : null; return (