From 3d72e166edf0f045635ea305e8c9c2ff77630a4e Mon Sep 17 00:00:00 2001 From: ludan <251918489@qq.com> Date: Wed, 17 Jun 2026 18:47:10 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E7=94=B5=E5=95=86?= =?UTF-8?q?=E5=85=A8=E5=9C=BA=E6=99=AF=E7=B4=A0=E6=9D=90=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E4=BD=93=E9=AA=8C=EF=BC=8C=E5=85=88=E6=9C=AC=E5=9C=B0=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E5=86=8D=E5=90=8E=E5=8F=B0=E4=B8=8A=E4=BC=A0=20OSS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 createLocalImageItems 同步创建本地 blob 预览项 - 新增 uploadImageItem 后台异步上传 OSS 并读取图片尺寸 - 改造商品主图、套图、参考图、服饰图、详情图 5 个上传入口 - 选择文件后立即渲染缩略图,OSS 上传在后台并行进行 - 上传完成后按 id 替换为 OSS URL,释放本地 blob URL --- src/features/ecommerce/EcommercePage.tsx | 209 ++++++++++++++++------- 1 file changed, 150 insertions(+), 59 deletions(-) diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index e703ea0..e7dafd2 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -1559,6 +1559,42 @@ const blobToDataUrl = (blob: Blob): Promise => reader.readAsDataURL(blob); }); +function createLocalImageItems(files: File[], limit: number, prefix: string): CloneImageItem[] { + const selectedFiles = Array.from(files).slice(0, limit); + const stamp = Date.now(); + return selectedFiles.map((file, index) => { + const localPreviewUrl = URL.createObjectURL(file); + const mimeType = normalizeEcommerceImageMime(file.type); + return { + id: `${prefix}-${stamp}-${index}`, + src: localPreviewUrl, + name: file.name, + file, + format: getImageFileFormat(file), + mimeType, + }; + }); +} + +async function uploadImageItem(item: CloneImageItem): Promise<{ src?: string; ossKey?: string; width?: number; height?: number }> { + if (!item.file) return {}; + const mimeType = normalizeEcommerceImageMime(item.file.type); + try { + const uploadBlob = item.file.type === mimeType ? item.file : new Blob([item.file], { type: mimeType }); + const [uploaded, dimensions] = await Promise.all([ + aiGenerationClient.uploadAssetBinary(uploadBlob, { + name: item.file.name, + mimeType, + scope: ecommerceOssScopes.productSource, + }), + readImageDimensions(item.src).catch(() => ({})), + ]); + return { src: uploaded.url, ossKey: uploaded.ossKey, ...dimensions }; + } catch { + return {}; + } +} + async function createUploadedImageItems(files: File[], limit: number, prefix: string): Promise { const selectedFiles = Array.from(files).slice(0, limit); const stamp = Date.now(); @@ -2553,20 +2589,30 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { }); }; - const addSetImages = async (files: File[]) => { + const addSetImages = (files: File[]) => { if (setImages.length >= 3) return; const imageFiles = notifyRejectedImages(files); if (!imageFiles.length) return; - try { - const nextImages = await createUploadedImageItems(imageFiles, 3 - setImages.length, "set"); - setSetImages((current) => { - if (current.length >= 3) return current; - return nextImages.length ? [...current, ...nextImages].slice(0, 3) : current; - }); - setProductSetStatus("ready"); - } catch (err) { - toast.error(err instanceof Error ? err.message : "商品图上传失败"); - } + const remainingSlots = 3 - setImages.length; + if (remainingSlots <= 0) return; + + const localItems = createLocalImageItems(imageFiles, remainingSlots, "set"); + setSetImages((current) => [...current, ...localItems].slice(0, 3)); + setProductSetStatus("ready"); + + Promise.all(localItems.map(async (item) => { + const uploaded = await uploadImageItem(item); + if (uploaded.src) URL.revokeObjectURL(item.src); + return { id: item.id, uploaded }; + })).then((results) => { + const updateMap = new Map(results.map((result) => [result.id, result.uploaded])); + setSetImages((current) => current.map((item) => { + const update = updateMap.get(item.id); + return update ? { ...item, ...update } : item; + })); + }).catch(() => { + toast.error("套图后台上传失败,请检查网络后重试"); + }); }; const handleSetUpload = (event: ChangeEvent) => { @@ -3651,20 +3697,33 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { }); }; - const addProductImages = async (files: File[]) => { + const addProductImages = (files: File[]) => { const imageFiles = notifyRejectedImages(files); if (!imageFiles.length) return; - try { - const nextImages = await createUploadedImageItems(imageFiles, maxCloneProductImages - productImages.length, "product"); - setProductImages((current) => { - if (current.length >= maxCloneProductImages) return current; - return nextImages.length ? [...current, ...nextImages].slice(0, maxCloneProductImages) : current; - }); - setStatus("ready"); - setResults([]); - } catch (err) { - toast.error(err instanceof Error ? err.message : "商品图上传失败"); - } + const remainingSlots = maxCloneProductImages - productImages.length; + if (remainingSlots <= 0) return; + + const localItems = createLocalImageItems(imageFiles, remainingSlots, "product"); + setProductImages((current) => [...current, ...localItems].slice(0, maxCloneProductImages)); + setStatus("ready"); + setResults([]); + + Promise.all(localItems.map(async (item) => { + const uploaded = await uploadImageItem(item); + if (uploaded.src) { + URL.revokeObjectURL(item.src); + } + return { id: item.id, uploaded }; + })).then((results) => { + const updateMap = new Map(results.map((result) => [result.id, result.uploaded])); + setProductImages((current) => current.map((item) => { + const update = updateMap.get(item.id); + if (!update) return item; + return { ...item, ...update }; + })); + }).catch(() => { + toast.error("商品图后台上传失败,请检查网络后重试"); + }); }; const handleProductUpload = (event: ChangeEvent) => { @@ -3718,22 +3777,29 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { }); }; - const addCloneReferenceImages = async (files: File[]) => { + const addCloneReferenceImages = (files: File[]) => { const imageFiles = notifyRejectedImages(files); if (!imageFiles.length) return; const remainingSlots = maxCloneReferenceImages - cloneReferenceImages.length; if (remainingSlots <= 0) return; - try { - const nextImages = await createUploadedImageItems(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); - } catch (err) { - toast.error(err instanceof Error ? err.message : "参考图上传失败"); - } + + const localItems = createLocalImageItems(imageFiles, remainingSlots, "reference"); + setCloneReferenceImages((current) => [...current, ...localItems].slice(0, maxCloneReferenceImages)); + hydrateCloneReferenceImageMeta(localItems); + + Promise.all(localItems.map(async (item) => { + const uploaded = await uploadImageItem(item); + if (uploaded.src) URL.revokeObjectURL(item.src); + return { id: item.id, uploaded }; + })).then((results) => { + const updateMap = new Map(results.map((result) => [result.id, result.uploaded])); + setCloneReferenceImages((current) => current.map((item) => { + const update = updateMap.get(item.id); + return update ? { ...item, ...update } : item; + })); + }).catch(() => { + toast.error("参考图后台上传失败,请检查网络后重试"); + }); }; const removeCloneReferenceImage = (imageId: string) => { @@ -4189,23 +4255,38 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { }; }, [openCloneModelSelect]); + const addGarmentImages = (files: File[]) => { + const uploadedFiles = notifyRejectedImages(files); + if (!uploadedFiles.length) return; + const remainingSlots = 5 - garmentImages.length; + if (remainingSlots <= 0) return; + + const localItems = createLocalImageItems(uploadedFiles, remainingSlots, "garment"); + setGarmentImages((current) => [...current, ...localItems].slice(0, 5)); + setTryOnStatus("ready"); + + Promise.all(localItems.map(async (item) => { + const uploaded = await uploadImageItem(item); + if (uploaded.src) URL.revokeObjectURL(item.src); + return { id: item.id, uploaded }; + })).then((results) => { + const updateMap = new Map(results.map((result) => [result.id, result.uploaded])); + setGarmentImages((current) => current.map((item) => { + const update = updateMap.get(item.id); + return update ? { ...item, ...update } : item; + })); + }).catch(() => { + toast.error("服饰图后台上传失败,请检查网络后重试"); + }); + }; + const handleGarmentUpload = (event: ChangeEvent) => { const files = event.target.files; - if (!files?.length) return; - const uploadedFiles = notifyRejectedImages(Array.from(files)); - if (!uploadedFiles.length) { + if (!files?.length) { event.target.value = ""; return; } - void (async () => { - try { - const nextImages = await createUploadedImageItems(uploadedFiles, 5 - garmentImages.length, "garment"); - setGarmentImages((current) => [...current, ...nextImages].slice(0, 5)); - setTryOnStatus("ready"); - } catch (err) { - toast.error(err instanceof Error ? err.message : "服饰图上传失败"); - } - })(); + addGarmentImages(Array.from(files)); event.target.value = ""; }; @@ -4241,20 +4322,30 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { reader.readAsDataURL(blob); }); - const addDetailImages = async (files: File[]) => { + const addDetailImages = (files: File[]) => { const uploadedFiles = notifyRejectedImages(files); if (!uploadedFiles.length) return; - try { - const nextImages = await createUploadedImageItems(uploadedFiles, 3 - detailProductImages.length, "detail"); - setDetailProductImages((current) => { - if (current.length >= 3) return current; - return nextImages.length ? [...current, ...nextImages].slice(0, 3) : current; - }); - setDetailStatus("ready"); - setDetailResultUrl(null); - } catch (err) { - toast.error(err instanceof Error ? err.message : "详情图上传失败"); - } + const remainingSlots = 3 - detailProductImages.length; + if (remainingSlots <= 0) return; + + const localItems = createLocalImageItems(uploadedFiles, remainingSlots, "detail"); + setDetailProductImages((current) => [...current, ...localItems].slice(0, 3)); + setDetailStatus("ready"); + setDetailResultUrl(null); + + Promise.all(localItems.map(async (item) => { + const uploaded = await uploadImageItem(item); + if (uploaded.src) URL.revokeObjectURL(item.src); + return { id: item.id, uploaded }; + })).then((results) => { + const updateMap = new Map(results.map((result) => [result.id, result.uploaded])); + setDetailProductImages((current) => current.map((item) => { + const update = updateMap.get(item.id); + return update ? { ...item, ...update } : item; + })); + }).catch(() => { + toast.error("详情图后台上传失败,请检查网络后重试"); + }); }; const uploadCloneImages = async (images: CloneImageItem[]): Promise => {