如何实现支持独立压缩预览的图片上传系统?
图片上传预览系统实现方案
针对你提出的「图片压缩转Base64后独立显示预览」需求,以下是优化后的完整实现代码及说明:
1. Zustand 状态管理完善
扩展Store方法,处理图片添加、删除及主图唯一性逻辑:
import { create } from "zustand"; export interface Picture { id: number; file: string | File; isMain: boolean; } interface Store { pictures: Picture[]; addPicture: (picture: Picture) => void; deletePicture: (id: number) => void; setMainPicture: (id: number) => void; } const useArticleFormStore = create<Store>((set) => ({ pictures: [], addPicture: (picture) => { set((state) => { const newPicture = { ...picture, isMain: state.pictures.length === 0 ? true : picture.isMain }; let updatedPictures = [...state.pictures, newPicture]; // 确保主图唯一 if (newPicture.isMain) { updatedPictures = updatedPictures.map(p => ({ ...p, isMain: p.id === newPicture.id })); } return { pictures: updatedPictures }; }); }, deletePicture: (id) => { set((state) => { const updatedPictures = state.pictures.filter(p => p.id !== id); // 删除主图后自动设置剩余第一张为主图 if (updatedPictures.length > 0 && state.pictures.some(p => p.id === id && p.isMain)) { updatedPictures[0].isMain = true; } return { pictures: updatedPictures }; }); }, setMainPicture: (id) => { set((state) => ({ pictures: state.pictures.map(p => ({ ...p, isMain: p.id === id })) })); }, })); export default useArticleFormStore;
2. 文件上传区域组件优化
修正主图判断逻辑,添加图片格式限制,实现独立异步处理:
import useArticleFormStore from "@/stores/addArticleStore"; import { ImagePlus } from "lucide-react"; import FileItem from "./FileItem"; import Btn from "./Btn"; import { compressImage } from "@/utils/compressor"; import { fileToBase64 } from "@/utils/fileToBase64"; export default function FileArea() { const pictures = useArticleFormStore((s) => s.pictures); const addPicture = useArticleFormStore((s) => s.addPicture); async function handleChange(e: React.ChangeEvent<HTMLInputElement>) { const selectedFiles = e.target.files; if (!selectedFiles || selectedFiles.length === 0) return; Array.from(selectedFiles).forEach(async (file, index) => { const id = Date.now() + index; try { const compressedFile = (await compressImage(file, { quality: 0.7, maxWidth: 500, maxHeight: 500, })) as File; const base64 = await fileToBase64(compressedFile); // 仅当上传列表为空时,第一张图片设为主图 addPicture({ id, file: base64, isMain: pictures.length === 0 && index === 0, }); } catch(err){ console.error("图片处理失败:", err); } }); // 清空input,允许重复选择相同文件 e.target.value = ""; } return ( <div className="grid md:grid-cols-[290px,1fr] max-md:gap-2 max-w-[900px] mx-auto"> <input type="file" name="picture" onChange={handleChange} multiple accept="image/*" className="absolute left-[-99999999999999999px]" id="picture" /> <label htmlFor="picture" className="border-black border-2 border-dashed flex flex-col justify-center items-center h-[200px] cursor-pointer hover:bg-gray-50 transition-colors" > <ImagePlus size={32} /> <p className="text-xs mt-2 cursor-pointer">添加产品图片</p> </label> <div className="border md:pl-3 flex flex-wrap gap-2"> {pictures.map((picture) => ( <FileItem picture={picture} key={picture.id} /> ))} </div> <Btn /> </div> ); }
3. 图片预览项组件实现
完善删除和设为主图功能:
import Img from "@/components/Img"; import { Picture } from "@/stores/addArticleStore"; import { Trash2 } from "lucide-react"; import useArticleFormStore from "@/stores/addArticleStore"; export default function FileItem({ picture }: { picture: Picture }) { const deletePicture = useArticleFormStore((s) => s.deletePicture); const setMainPicture = useArticleFormStore((s) => s.setMainPicture); return ( <div className="group w-[110px] h-[110px] relative"> <button className="hidden group-hover:block absolute top-1 right-1 bg-red-500 text-white p-1 rounded-full hover:bg-red-600 transition-colors z-10" title="删除" aria-label="删除图片" onClick={() => deletePicture(picture.id)} > <Trash2 size={18} /> </button> {picture && typeof picture.file === "string" && ( <Img src={picture.file} alt="预览图" className="w-[110px] h-[110px] object-cover rounded-md" /> )} <p className={`absolute bottom-0 text-center w-full p-1 rounded-md text-sm font-medium text-white bg-[#00000080] ${picture.isMain ? "" : "hidden group-hover:block text-xs"}`} > {picture.isMain ? ( <span>主图</span> ) : ( <span onClick={() => setMainPicture(picture.id)} className="cursor-pointer underline hover:text-blue-200 transition-colors" aria-label="设为主图" > 设为主图 </span> )} </p> </div> ); }
核心功能说明
- 独立异步处理:通过
forEach遍历文件,每个文件单独执行压缩和转Base64操作,完成后立即更新状态,实现单张图片处理完成即显示预览,无需等待其他图片。 - 主图唯一性保障:设置新主图时自动取消其他图片的主图标记;删除主图后自动将剩余第一张设为主图。
- 用户体验优化:限制仅上传图片格式,允许重复选择相同文件,添加hover状态过渡效果。
内容的提问来源于stack exchange,提问作者Kevin
相关产品推荐
相关产品推荐

