在react中手搓一个antd的Upload文件上传组件
前言
- 下面所说的照片列表是模仿antd的Upload来进行实现的
- 本来是直接用antd的Upload的,但是我发现需要对里面的icon进行更改的时候,很困难,迫不得已,自己实现了一个
- 但是功能没有Upload那么强大,并且到最后也没有实现的是“Upload在增删照片之后,相邻照片框的移动是渐进的,就是那种移动过去的感觉”,也没有做到上传两张相同的照片
实现过程
input文件上传
- 最基本的,首先我们要有一个input用来上传照片
- type:指定为文件上传
- accept:指定接受的文件类型
- onChange:当上传的文件不一样时触发,参数是一个File类型的对象,也就是你上传的文件
<input type="file" accept='.png, .svg, .jpg, .jpeg, .jfif' onChange={handleGalleryChange}/>
onChange事件触发
- 我们要接受上传的文件
- 因为我们要实现的是一个文件列表,所以我们需要一个数组去存放这些文件
- 再考虑到动态生成元素,所以我们选择用状态来存放这个数组
- 而代码中的图库文件类型,是根据后面的需要列出来的:name、用来识别另外一个图库中的照片是否相同、thumbUrl用来赋值给img展示出来、lastModified用来找出本图库中的自己
- 因为要将上传的照片显示出来,所以要有一个dataUrl用来给img的src进行赋值,然而onChange接受的参数是一个File类型的Blob,所以将Blob转换成dataUrl,这里的getBase64是antd给的,我只是顺手拿来用了
- 因为要动态生成列表,所以我们需要借助galleryFileList这个状态,状态改变监听的是引用,所以我们这里用的是深拷贝,将新的数组添加元素后再重新赋给状态,状态就监听到改变了
- 我们这里还有一个存入到本地的,是想要再打开但是记录还在的效果
type galleryFile = {
name: string,
thumbUrl: string,
lastModified: number
}
const [galleryFileList, setGalleryFileList] = useState<galleryFile[]>(local_galleryFileList)
const handleGalleryChange = (e: any) => {
getBase64(e.target.files[0]).then((res)=>{
const fileList = galleryFileList ? [...galleryFileList] : [];
fileList.push({
thumbUrl: res,
name: e.target.files[0].name,
lastModified: e.target.files[0].lastModified
})
setGalleryFileList(fileList)
localStorage.setItem('AICase_gallery_fileList', JSON.stringify(fileList))
})
}
const getBase64 = (file: RcFile): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
样式实现
- 下面实现的是一个还没有文件上传时的上传框,那个uploadButton单独放出来是为了方便配置框里面的内容以及样式
- 主要的样式可以参考一下antd的Upload,就一个正正方方的虚线框,里面有一天“上传”的文字以及一个代表添加的“+”,鼠标放进去之后会有过渡的虚线框变量效果
- 值得注意的是,这个框并不是用input改变样式形成的,input的样式本身想要改变比较麻烦,我这里将input铺满整个框,并将opacity调成0,并放在最上面,这样的话我们既可以点到input,触发相应的事件,又可以弄个好看的样式
// HTML结构
<div className="upload_container">
<div className="upload">
<input type="file" accept='.png, .svg, .jpg, .jpeg, .jfif' onChange={handleGalleryChange}/>
{uploadButton}
</div>
</div>
</div>
const uploadButton = (
<div className='uploadBtn'>
<PlusOutlined />
<div style={{ marginTop: 8 }}>上传</div>
</div>
);
// css样式
.upload_container {
width: 102px;
height: 102px;
margin-right: 8px;
margin-bottom: 8px;
box-sizing: border-box;
.upload {
position: relative;
width: 102px;
height: 102px;
padding: 8px;
border-radius: 8px;
border: 1px dashed #d9d9d9;
background-color: rgba(0, 0, 0, 0.02);
box-sizing: border-box;
cursor: pointer;
transition: border-color 0.3s;
input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
opacity: 0;
z-index: 1;
}
.uploadBtn {
position: absolute;
top: 25%;
left: 0;
text-align: center;
width: 100%;
height: 100%;
color: rgba(0, 0, 0, 0.88);
font-size: 14px;
}
img {
width: 100%;
height: 100%;
object-fit: contain;
}
&:hover {
border: 1px dashed rgba(250, 173, 20);
}
}
}
动态生成
- 下面这一段代码就是动态生成的
- 用react来进行动态生成是挺方便的,我们这里直接利用刚才的galleryFileList状态来进行生成新的照片框,而用来上传的照片框是固定不动的,这也为我们后面“不能连续上传两张相同的照片埋下了隐患”,因为那个文件input一直都在那里,只是我们看不见罢了,后面可以考虑清理一下input的value之类的,解决这个问题
{galleryFileList ? galleryFileList.map((value: galleryFile, index: number) => {
return (
<div className="upload_container" onClick={(e)=>handleGalleryClick(e)} key={`upload-container-${index}`}>
<div className="upload upload_active">
<img src={value.thumbUrl} alt="" width='102px' height='102px' className='uploadImg' data-name={value.name} data-lastmodified={value.lastModified} />
<div className="mask">
<CheckOutlined className='icon checkOutIcon' />
<DeleteOutlined className='icon deleteIcon'/>
</div>
</div>
</div>)
}) : ''}
<div className="upload_container">
<div className="upload">
<input type="file" accept='.png, .svg, .jpg, .jpeg, .jfif' onChange={handleGalleryChange}/>
{uploadButton}
</div>
</div>
源码
import type { RcFile } from 'antd/es/upload';
export const getBase64 = (file: RcFile): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
import { PlusOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons'
import type { UploadFile } from 'antd/es/upload/interface';
import React,{ useState } from 'react'
import { getBase64 } from '../../utils/dataChange'
import './index.scss'
type galleryFile = {
name: string,
thumbUrl: string,
lastModified: number
}
const uploadButton = (
<div className='uploadBtn'>
<PlusOutlined />
<div style={{ marginTop: 8 }}>上传</div>
</div>
);
const local_galleryFileList = JSON.parse(localStorage.getItem('AICase_gallery_fileList') as string)
export default function UploadList(props:{presetFileList: UploadFile[], setSpotResult: any, setIsModalOpen: any}) {
const {presetFileList, setSpotResult, setIsModalOpen} = props
const [galleryFileList, setGalleryFileList] = useState<galleryFile[]>(local_galleryFileList)
const showModal = () => {
setIsModalOpen(true);
};
const handleGalleryChange = (e: any) => {
getBase64(e.target.files[0]).then((res)=>{
const fileList = galleryFileList ? [...galleryFileList] : [];
fileList.push({
thumbUrl: res,
name: e.target.files[0].name,
lastModified: e.target.files[0].lastModified
})
setGalleryFileList(fileList)
localStorage.setItem('AICase_gallery_fileList', JSON.stringify(fileList))
})
}
const handleGalleryClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const target = e.target as HTMLDivElement
const currentTarget = e.currentTarget as HTMLDivElement
const img = currentTarget.querySelector('img') as HTMLImageElement
if(target.classList.value.includes("deleteIcon")){
const result = galleryFileList.filter((obj:galleryFile) => obj.lastModified.toString() !== img.getAttribute('data-lastmodified'))
setGalleryFileList(result)
}
else if(target.classList.value.includes("checkOutIcon")){
const isExist = presetFileList.some((obj: UploadFile) => obj.name === img.getAttribute('data-name'))
setSpotResult(isExist)
showModal()
}
}
return (
<div className="upload_list">
{galleryFileList ? galleryFileList.map((value: galleryFile, index: number) => {
return (
<div className="upload_container" onClick={(e)=>handleGalleryClick(e)} key={`upload-container-${index}`}>
<div className="upload upload_active">
<img src={value.thumbUrl} alt="" width='102px' height='102px' className='uploadImg' data-name={value.name} data-lastmodified={value.lastModified} />
<div className="mask">
<CheckOutlined className='icon checkOutIcon' />
<DeleteOutlined className='icon deleteIcon'/>
</div>
</div>
</div>)
}) : ''}
<div className="upload_container">
<div className="upload">
<input type="file" accept='.png, .svg, .jpg, .jpeg, .jfif' onChange={handleGalleryChange}/>
{uploadButton}
</div>
</div>
</div>
)
}
// index.scss
.upload_list { // 文件列表
display: flex;
justify-content: start;
align-content: flex-start;
flex-wrap: wrap;
width: 100%;
.upload_container {
width: 102px;
height: 102px;
margin-right: 8px;
margin-bottom: 8px;
box-sizing: border-box;
.upload {
position: relative;
width: 102px;
height: 102px;
padding: 8px;
border-radius: 8px;
border: 1px dashed #d9d9d9;
background-color: rgba(0, 0, 0, 0.02);
box-sizing: border-box;
cursor: pointer;
transition: border-color 0.3s;
input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
opacity: 0;
z-index: 1;
}
.uploadBtn {
position: absolute;
top: 25%;
left: 0;
text-align: center;
width: 100%;
height: 100%;
color: rgba(0, 0, 0, 0.88);
font-size: 14px;
}
img {
width: 100%;
height: 100%;
object-fit: contain;
}
&:hover {
border: 1px dashed rgba(250, 173, 20);
}
}
.upload_active { // 上传后的文件列表
border: 1px solid #d9d9d9;
[fill=currentColor] { // antd内置的颜色修改
color: rgba(255, 255, 255, 0.65);
}
.mask { // 遮罩层
position: absolute;
top: 8px;
left: 8px;
width: 84px;
height: 84px;
background-color: rgba(0,0,0,.45);
text-align: center;
line-height: 84px;
opacity: 0;
transition: opacity .3s;
.icon { // 操作icon
position: relative;
transition: background-color color .3s;
padding: 3px;
[fill=currentColor]:hover {
color: #fff;
}
&:hover {
border-radius: 3px;
background-color: rgba(0,0,0,.1);
}
&::after { // 这个伪类是用来确保点击事件的target指向该icon而不是icon里面的什么
content: "";
position: absolute;
top: 0;
left: 0;
width: 22px;
height: 22px;
background-color: rgba(255, 255, 255, 0);
}
}
.deleteIcon {
margin-left: 8px;
}
&:hover {
opacity: 1;
}
}
&:hover {
border: 1px solid #d9d9d9;
}
}
}
}