之前一直使用antd的upload组件,受antd启发,自己简单实现一个类似的文件上传组件,可以进行普通上传,拖拽上传,取消上传功能
TODO: 大文件上传和断点续传待补充
1. 实现文件上传功能
import React, { ChangeEvent, useRef, useState } from 'react';
import logo from './logo.svg';
import axios from 'axios'
import { Button } from 'antd';
import './App.css';
import { ButtonSize, ButtonType} from './ButtonCom/Button'
import UploadList from './UploadList'
import Dragger from './dragger'
import { DownloadOutlined } from '@ant-design/icons';
class Images {
public src: string = 'https://www.google.com.hk/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png'
public alt: string = '谷歌'
public width: number = 500
}
type propsName = keyof(Images)
interface Obj {
[key: string]:any;
}
interface User {
username: string
id: number
token: string
avatar: string
role: string
}
// 变成可选的
type Partial<T> = {
[P in keyof T]? : T[P]
};
type Partials<T> = {
[P in keyof T]?: T[P]
}
type PartialProps = Partial<User>
export type UploadFileStatus = 'ready' | 'uploading' | 'success' | 'error'
export interface UploadFile {
uid: string;
size: number;
name: string;
status?: UploadFileStatus;
percent?: number;
raw?: File;
response?: any;
error?: any;
}
interface PropsType {
// 上传之前的操作
beforeUpload?: (file: File) => boolean | Promise<File>
// 额外的参数
data?: {[key: string]: any}
// name: 上传文件时 对应的key值,默认为 file: file
name?: string
// 上传的地址
action: string
// headers
headers?: {[key: string]: any}
// 跨域是否需要凭证,axios默认为false
withCredentials?: boolean
// 移除时候的回调
onRemove?: (file: File) => void;
// 上传成功的回调
onSuccess?: (data: any, file: File) => void;
// 上传失败的回调
onError?: (err: any, file: File) => void;
// 上传改变的回调
onChange?: (file: File) => void;
// 是否允许拖拽
drag?: boolean
}
const App: React.FC<PropsType> = (props) => {
const { beforeUpload, data, name, action, headers, withCredentials, onRemove, onSuccess, onError, onChange, children,
drag, } = props;
const [fileList, setFileList] = useState<any>([{
uid: 1,
size: 2343,
name: 'test',
status: 'uploading',
percent: 75,
// raw?: File;
// response?: any;
// error?: any;
}])
const InputRef = useRef<HTMLInputElement>(null)
const handleClick = () => {
if (InputRef.current) {
InputRef.current.click();
}
}
const handleRemove = (File: any) => {
console.log('点击要移除的文件',File)
setFileList((prevList:any) => {
return prevList.filter((item:any) => item.uid !== File.uid)
})
// 取消上传
// 将取消请求函数保存到file文件里面,删除的时候调用这个请求
if ( typeof(File.cancels) === 'function') {
File.cancels()
}
if (onRemove) {
onRemove(File)
}
// if (onRemove) {
// onRemove(file)
// }
}
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
// 获取到文件
const Files = e.target.files
// INput 没有上传直接退出
if (!Files) {
return
}
uploadFiles(Files)
// 防止上传相同文件不会触发onChange, 清楚上传值
if (InputRef.current) {
InputRef.current.value=""
}
}
const uploadFiles = (Files: any) => {
console.log(Files,'---')
let postFiles = Array.from(Files)
console.log(postFiles)
postFiles.forEach((item:any) => {
// 如果是图片。将图片以base64格式展示出来
// const reader = new FileReader();
// reader.readAsDataURL(item)
// reader.onload = (e:any) => {
// console.log(e.target.result)
// const ImgElement = document.createElement('img')
// ImgElement.src = e.target.result;
// document.body.appendChild(ImgElement)
// }
// beforeUpload 不存在 则直接上传
if (!beforeUpload) {
post(item)
} else {
const result = beforeUpload(item)
if (result && result instanceof Promise) {
result.then(processFile => {
post(processFile)
})
} else if (result !== false) {
post(item)
}
}
})
}
const post = (file:any) => {
// 用于取消上传
let cancels:any
// 要保存文件上传的信息
let _file = {
uid: Date.now() + 'upload-file',
name: file.name,
size: file.size,
status: 'ready',
percent: 0,
raw: file,
// 保存取消上传的函数
cancels: cancels
}
// 将要上传的文件以及信息保存到filelist里面,用于上传列表展示
setFileList((prevList:any) => {
return [_file, ...prevList]
})
// 利用formData 数据
const formData = new FormData()
formData.append(name || 'file', file )
// 如果有传入额外的参数(data),也添加到formData中
if (data) {
Object.keys(data).forEach(item => {
formData.append(item, data[item])
})
}
// 开始上传文件
// 利用axioa
axios.post(action, formData, {
headers: {
...headers,
'Content-Type': 'multipart/form-data'
},
// 取消请求
cancelToken: new axios.CancelToken((c) => {
// 让状态为ready的时候也可以取消上传
updataFileList(_file, { cancels : c })
cancels = c
}),
// 表示跨域请求时候是否需要使用凭证
withCredentials,
// 上传处理进度事件
onUploadProgress: (e) => {
let procentTage = Math.round((e.loaded * 100) / e.total) || 0
if (procentTage < 100) {
// 更新fileList的值 并且将取消上传的函数也保存下来,用于取消上传
updataFileList(_file, { percent: procentTage, status: 'uploading', cancels : cancels })
}
}
}).then(resp => {
updataFileList(_file,{ status: 'success', response: resp.data })
if (onSuccess) {
onSuccess(resp.data, file)
}
if (onChange) {
onChange(file)
}
}).catch(err => {
updataFileList(_file, {status: 'error', error: err})
if (onError) {
onError(err, file)
}
if (onChange) {
onChange(file)
}
})
}
// 更新fileLIst列表,进度以及状态
function updataFileList(file: any,updateObj: any) {
setFileList((prev:any) => {
return prev.map((item: any) => {
if (file.uid === item.uid) {
return {...file, ...updateObj }
} else {
return item
}
})
})
}
return (
<div className="App">
<header>
<div>
{/* <img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<Button> 默认的 </Button>
<Button href="xxxx" btnType={ButtonType.Link}> 第一个</Button> */}
<div>
<Button shape="round" icon={<DownloadOutlined />} onClick={handleClick}>
上传文件
{/* {drag ?
<Dragger onFile={(files) => {uploadFiles(files)}}>
{children}
</Dragger>:
children
} */}
<input
style={{ display: 'none' }}
type="file"
ref={InputRef}
onChange={handleInputChange}
multiple
/>
</Button>
{
<Dragger onFile={(files) => {uploadFiles(files)}}>
{children}
<a onClick={handleClick}>
点此上传
</a>
</Dragger>
}
<UploadList
fileList={fileList}
onRemove={handleRemove}
/>
{/* <Button type="primary">Button</Button> */}
{/* <div id="drag" draggable="true">zzzz</div> */}
</div>
</div>
</header>
</div>
);
}
export default App;
利用axios实现上传接口请求
2. 展示上传文件信息列表
展示的文件信息应该展示 1.上传进度 2.重新上传 3.文件名
注意: 点击重新上传的时候,需要取消掉接口,因为是利用axios上传,那么我们直接利用axios的取消请求
axios 取消请求 方法一:
import React from 'react'
import { Progress,Button, Space, Tooltip } from 'antd'
import { DeleteOutlined } from '@ant-design/icons';
interface Props {
fileList?: any;
onRemove?: any;
}
const UploadList = (props: Props) => {
const { fileList, onRemove } = props;
console.log(fileList, '文件list')
return (
<>
<div>
上传文件列表
</div>
<ul>
{
fileList.map((item:any) => {
return (
<div key={item.uid}>
{/* <div style={{ width: 100 }}>
{item.name}
</div> */}
<Space>
<span>
<Tooltip title={item.name}>
{(item.status === 'uploading' || item.status === 'ready') && (<span>上传中</span>)}
{item.status === 'success' && (<span>上传成功</span>)}
{item.status === 'error' && (<span>上传失败</span>)}
</Tooltip>
</span>
<div style={{ width: 200 }}>
{
item.status === 'uploading' && (
<Progress percent={item.percent || 0} />
)
}
{
item.status === 'success' && (
<Progress percent={100} />
)
}
{
item.status === 'ready' && (
<Progress percent={0} />
)
}
{
item.status === 'error' && (
<Progress percent={item.percent || 100} status="exception" />
)
}
</div>
<Tooltip title={ item.status === 'uploading' || item.status === 'ready'? '取消上传' : '重新上传'}>
<Button type="primary" shape="circle" icon={<DeleteOutlined />} onClick={() => onRemove(item)} />
</Tooltip>
</Space>
</div>
)
})
}
</ul>
</>
)
}
export default UploadList
3. 实现拖拽上传
利用H5新增的拖拽方法
元素在拖放过程中触发的事件 HTML5中,只要将元素的 draggable 属性设置为 true 就可以实现拖放功能
<div draggable="true">实现拖拽</div>
在拖放过程中,触发了多个事件,如下:
dragstart:事件主体是被拖放元素,在开始拖放被拖放元素时触发。
drag:事件主体是被拖放元素,在正在拖放被拖放元素时触发。
dragenter:事件主体是目标元素,在被拖放元素进入某元素时触发。
dragover:事件主体是目标元素,在被拖放在某元素内移动时触发。
dragleave:事件主体是目标元素,在被拖放元素移出目标元素是触发。
drop:事件主体是目标元素,在目标元素完全接受被拖放元素时触发。
dragend:事件主体是被拖放元素,在整个拖放操作结束时触发。
以上我们看到拖动总共有7个事件,其中事件主体是拖放元素的是,dragstart(开始拖动) 、drag(正在拖放) 、dragend(拖放结束),其他4个事件主体都是目标元素,进入、移动、离开、完全进入四个状态。
简单的一个小例子来了解drag
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#divTarget {
width: 300px;
height: 300px;
border: 5px solid red;
}
</style>
</head>
<body>
<div id="dragText" draggable="true">这是可拖动的元素</div>
<img src="./404.gif" class="dragimg" alt="" style="width: 200px; height: 200px;">
<div id="divTarget">
xx
</div>
<script>
let TargetBox = document.getElementById('divTarget')
let dragtexts = document.getElementById('dragText')
// 元素开始拖动时
dragtexts.addEventListener('dragstart', (event) => {
// dataTransfer.setData() 来添加数据
// dataTransfer.getData() 来获取数据 在drop中才能拿到值
event.dataTransfer.setData('keyss', '拖拽保存的数据')
console.log('元素开始拖动时')
})
// 元素正在拖动时
dragtexts.addEventListener('drag', (event) => {
console.log('元素正在拖动时')
})
// 元素拖动结束
dragtexts.addEventListener('dragend', (event) => {
console.log('元素拖动结束')
})
// 进入区域
TargetBox.addEventListener('dragenter', (event) => {
console.log('进入到此区域')
})
// 离开区域
TargetBox.addEventListener('dragleave', (event) => {
console.log('离开区域')
})
// 区域拖动
TargetBox.addEventListener('dragover', (event) => {
event.preventDefault(); // 如果要放到区域必须使用这个
console.log('区域拖动')
})
// 放到区域
TargetBox.addEventListener('drop', (event) => {
console.log('放到区域')
console.log(event.dataTransfer.getData('keyss'))
})
</script>
</body>
</html>
然后来写dragger组件
组件里面也可以引用 classNames 来进行控制样式的更改,这里样式没啥要求,就简单写下
import React, { FC, useState, DragEvent } from 'react'
import classNames from 'classnames'
interface DraggerProps {
onFile: (files: FileList) => void;
}
export const Dragger: FC<DraggerProps> = (props) => {
const { onFile, children } = props
const [ dragOver, setDragOver ] = useState(false)
// 使用方法 true 的时候加上这个类,false 不加这个类
const klass = classNames('viking-uploader-dragger', {
'is-dragover': dragOver
})
// 拖拽的元素放到了目标元素上
const handleDrop = (e: DragEvent<HTMLElement>) => {
e.preventDefault()
setDragOver(false)
// 将拖拽获取到的文件传出去
onFile(e.dataTransfer.files)
}
// 用来判断目标元素
const handleDrag = (e: DragEvent<HTMLElement>, over: boolean) => {
console.log('drageover' )
e.preventDefault()
setDragOver(over)
}
return (
<div
className={klass}
style={{background: dragOver ? 'red' : '#ccc' , width: 200, height: 200, border: dragOver ? '5px dashed black' : '1px solid #fff', margin: '0 auto'}}
// 在目标元素移动中触发
onDragOver={e => { handleDrag(e, true)}}
// 在离开目标元素触发
onDragLeave={e => { handleDrag(e, false)}}
// 把拖拽元素放到目标元素上触发
onDrop={handleDrop}
>
{children}
<div style={{margin: '0 auto'}}>拖拽上传</div>
</div>
)
}
export default Dragger;