From 2b65206b84df4f28d53b6aa3e87b8146aa1c5410 Mon Sep 17 00:00:00 2001 From: ludan <251918489@qq.com> Date: Thu, 4 Jun 2026 17:27:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=B5=E5=95=86=E5=85=8B=E9=9A=86?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BA=A4=E4=BA=92=E5=8D=87=E7=BA=A7=E3=80=81?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E6=A8=A1=E5=9E=8B=E9=80=89=E6=8B=A9=E5=99=A8?= =?UTF-8?q?=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【电商克隆 - 商品图上传交互重构】 - 新增上传预览大图区(clone-ai-upload-preview-wrap),点击缩略图可切换预览 - 选中缩略图增加 is-active 绿色边框高亮 - 预览区显示商品图编号 + 尺寸/比例/格式信息(formatProductImageSpec) - 上传区到达 7 张上限时显示"已达上限"、阻止拖拽上传、输入框禁用 - 上传图片自动异步读取尺寸(width/height),无需等待上传完成即可展示 - 已上传素材区重构为列表头(标题+计数)+ 缩略图栈式布局 - 缩略图增加序号角标(1-7),删除按钮独立于缩略图下方 - selectedProductImageId 状态自动管理:删除/新增时自动切换到有效图片 【工作台 - 视频模型选择器图标】 - 新增 VIDEO_MODEL_ICON_URLS 映射(HappyHorse/Pixverse/Vidu/Wan/Kling) - SelectChip 组件在 chipId=video-model 时显示模型品牌图标 - getVideoModelIconUrl 支持中英文模糊匹配 【样式】 - ecommerce.css: 预览区/素材栈/缩略图选中态/上限态完整样式 - dark-green.css: 主题层微调 --- src/features/ecommerce/EcommercePage.tsx | 129 ++++++-- .../workbench/WorkbenchSelectChips.tsx | 26 +- src/styles/pages/ecommerce.css | 288 +++++++++++++++++- src/styles/themes/dark-green.css | 34 +++ 4 files changed, 450 insertions(+), 27 deletions(-) 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 (