【UI组件库】upload组件

需求分析

upload组件的一些样式:
在这里插入图片描述
一步步来看,点击“点击上传”按钮后,选择你想上传的文件,然后开始自动上传,展示进度条,进度条满了之后会显示图片名称及绿色的标志,鼠标浮在上面会展示×,删除已上传的文件,若上传失败,展示error.jpg及红色的标志在这里插入图片描述
上传一个文件的流程如下,点击“点击上传”按钮后,选择你想上传的文件,然后触发beforeUpload(file)函数,然后开始上传(onProgress(event, file)),上传完毕后触发onChange(file),若上传成功,触发onSuccess(response, file),上传失败触发onError(error, file),点击已经上传文件的删除按钮,调用onRemoved(file)。
在这里插入图片描述
在这里插入图片描述
据此,我们可以知道upload组件需要的属性:
在这里插入图片描述

使用Axios发送HTTP请求

fetch的缺点:

  1. 只对网络请求报错,对400 500都当作成功的请求
  2. 默认不带cookie
  3. 不支持abort,不支持超时控制
  4. 无法原生监测请求的进度

在线服务器:JSONPlaceholdermock(它可以根据你自己填入的JSON定制化输出,即response的结果是你自己输入的)
本节我们使用Axios发送HTTP请求,在项目中安装:npm install axios --save

上传文件的基本方式

两种方法完成文件的上传:

  1. 传统的表单提交的方式
import React,
import axios from 'axios'

const App: React.FC = () => {
	return (
		<div className='App' style={{marginTop:'100px',marginLeft:'100px'}}>
			<form method='post' encType='multipart/form-data' action='https://www.baid.com'>
				<input type='file' name='myFile' />
				<button type='submit'>Submit</button>
			</from>
		</div>
	)
}

当你要使用文件上传功能时,上传文件的表单和普通表单的不同之处在于type='file',文件和普通的字符串不同,它属于二进制格式,所以form要设置encType='multipart/form-data'(注意你要上传文件或你表单发送的内容中有二进制格式文件,都要设置这个),除此之外,encType的取值还有:
(1)application/x-www-form-urlencoded:默认,表单数据被encoded到body或Url中去传送,如果要发送大量二进制数据,使用这种形式是低效的
(2)multipart/form-data:发送二进制文件
(3)text/plain

  1. 使用JS发送异步请求的方式
import React,
import axios from 'axios'

const App: React.FC = () => {
	const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
		// e.target.files返回值是一个数组,数组里放着一个个文件数据,因为支持选择多个文件
		const files = e.target.files;
		if(files){
			const uploadedFile = file[0]
			//模拟表单的数据
			const formData = new FormData();
			// 使用formData的append方法为其附加input值
			// formData.append(文件名, 文件),数据会以键值对文件名:文件的形式保存起来
			formData.append(uploadedFile.name, uploadedFile)
			axios.post('https://jsonplaceholder.typicode.com/posts',formData,{
				headers:{
					// 这里的Content-Type就等同于form表单中的encType
					'Content-Type': 'multipart/form-data'
				}
			}).then(res => console.log(res))
		}
	}
	return (
		<div className='App' style={{marginTop:'100px',marginLeft:'100px'}}>
			<input type='file' name='myFile' onChange={handleFileChange}/>
		</div>
	)
}

编码实现上传文件基本流程

新建文件src/components/Upload/upload.tsx

import React,{FC, useRef} from 'react'
import axios from 'axios'
import Button from '../Button/button'
export interface UploadProps{
	action: string;
	beforeUpload?: (file: File) => boolean | Promise<File>;// 上传前我们要做两件事:1.验证2.对文件进行转换,所以返回值为Boolean或promise
	onProgress?: (percentage: number,file: File) => void;
	onSuccess?: (data: any, file: File) => void;
	onError?: (err: any, file: File) => void;
	onChange?: (file: File) => void;
}
export const Upload: FC<UploadProps> = (props) => {
	const {action, onProgress, onSuccess, onError} = props;
	const fileInput = useRef<HTMInputElement>(null)
	const handleClick = () => {
		if(fileInput.current){
			fileInput.current.click();
		}
	}
	// 值变化时选择文件
	const handleFileChange = (e:ChangeEvent<HTMLInputElement>) => {
		const file = e.target.files;
		if(!files) return;
		uploadFiles(files);
		if(fileInput.current){
			fileInput.current.value = '';
		}
	}
	// FileList是一个类数组对象,想在类数组上使用数组的方法,需要先将类数组转换为数组
	const uploadFiles = (files: FileList) => {
		let postFiles = Array.from(files)
		postFiles.forEach(file => {
			if(!beforeUpload){// 没有beforeUpload,直接上传
				post(file)
			}else{
				const result = beforeUpload(file)
				if(result && result instanceof Promise){
					result.then(res => post(res))
				}else if(result !== false) {
					post(result)
				}
			}
		})
	}
	const post = (file: File) => {
		const formData = new FormData()
		formData.append(file.name,file);
		axios.post(action, formData,{
			headers:{
				'Content-Type': 'multipart/form-data'
			}
			//此时我们需要拿到onProgress事件,axios给我们提供了一个onUploadProgress来显示上传进度
			onUploadProgress: (e) => {
				let percentage = Math.round((e.loaded * 100) / e.total) || 0;
				if(percentage < 100) {
					if(onProgress) {
						onProgress(percentage, file)
					}
				}
			}
		}).then(res => {
			console.log(res)
			if(onSuccess){
				onSuccess(res.data, file)//第一个参数是服务器返回的数据,第二个参数是file对象
			}
			if(onChange) {
				onChange(file)
			}
		}).catch(err => {
			console.error(err)
			if(onError){
				onError(err, file)//第一个参数是err对象,第二个参数是file对象
			}
			if(onChange) {
				onChange(file)
			}
		})
	}

	return (
		<div className='viking-upload-component'>
			<button btnType='primary' onClick={handleClick}}>Upload File</button>
			<input 
				className='viking-file-input' 
				style={{display: 'none'}} 
				ref={fileInput} 
				onChang={handleFileChange}
				type='file' />
		</div>
	)
}

创建列表数据

下面画勾的这些就是数据列表
在这里插入图片描述
先创建列表数据的类型:

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;
}

列表数据保存在state中:const [ fileList, setFileList ] = useState<UploadFile[]>([])
在上传开始的时候更新列表数据

// 更新列表的某一项(updateFile项),
const updateFileList = (updateFile: UploadFile, updateObj: Partial<UploadFile>) => {
    // 上传过程中更改状态。setFileList不仅可以接收新值,也可以接收函数,函数的参数就是当前的值
    setFileList(prevList => {
      	return prevList.map(file => {
        	if (file.uid === updateFile.uid) {
          		//用我们要更新的属性值,覆盖该项的属性值
          		return { ...file, ...updateObj }
        	} else {
          		return file
        	}
      	})
    })
}
const post = (file: File) => {
	let _file: UploadFile = {
      uid: Date.now() + 'upload-file',
      status: 'ready',
      name: file.name,
      size: file.size,
      percent: 0,
      raw: file
    }
    //更新数据列表
    setFileList([_file, ...fileList])
	const formData = new FormData()
	formData.append(file.name,file);
	axios.post(action, formData,{
		headers:{
			'Content-Type': 'multipart/form-data'
		}
		//此时我们需要拿到onProgress事件,axios给我们提供了一个onUploadProgress来显示上传进度
		onUploadProgress: (e) => {
			let percentage = Math.round((e.loaded * 100) / e.total) || 0;
			if(percentage < 100) {
				updateFileList(_file, { percent: percentage, status: 'uploading'})
				if(onProgress) {
					onProgress(percentage, file)
				}
			}
		}
	}).then(res => {
		console.log(res)
		updateFileList(_file, {status: 'success', response: resp.data})
		if(onSuccess){
			onSuccess(res.data, file)//第一个参数是服务器返回的数据,第二个参数是file对象
		}
		if(onChange) {
			onChange(file)
		}
	}).catch(err => {
		console.error(err)
		updateFileList(_file, { status: 'error', error: err})
		if(onError){
			onError(err, file)//第一个参数是err对象,第二个参数是file对象
		}
		if(onChange) {
			onChange(file)
		}
	})
}

显示上传的数据

在这里插入图片描述
根据文件信息展示不同的样式,以上是三个不同状态下的列表模式,分为左右两栏,左边是固定的文件图标,然后是文件名称,右边根据不同的状态添加三种不同的图标,假如是uploading的状态,还需要添加一个带百分比的进度条,当鼠标hover到条目上时,会显示×图标,点击该图标后会删除这个条目,并且触发用户自定义的onRemove事件。进度条我们本节先不做。
新建src/components/Upload/uploadList.tsx

import React, { FC } from 'react'
import { UploadFile } from './upload'
import Icon from '../Icon/icon'
interface UploadListProps {
  fileList: UploadFile[];
  onRemove: (_file: UploadFile) => void;
}

export const UploadList: FC<UploadListProps> = (props) => {
  const {
    fileList,
    onRemove,
  } = props

  return (
    <ul className="viking-upload-list">
      {fileList.map(item => {
        return (
          <li className="viking-upload-list-item" key={item.uid}>
            <span className={`file-name file-name-${item.status}`}>
              <Icon icon="file-alt" theme="secondary" />
              {item.name}
            </span>
            <span className="file-status">
              {(item.status === 'uploading' || item.status === 'ready') && <Icon icon="spinner" spin theme="primary" />}
              {item.status === 'success' && <Icon icon="check-circle" theme="success" />}
              {item.status === 'error' && <Icon icon="times-circle" theme="danger" />}
            </span>
            <span className="file-actions">
              <Icon icon="times" onClick={() => { onRemove(item)}}/>
            </span>
          </li>
        )
      })}
    </ul>
  )
}
export default UploadList;

将UploadList添加进Upload组件中:

import React,{FC, useRef} from 'react'
import axios from 'axios'
import Button from '../Button/button'
import UploadList from './uploadList'

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;
}

export interface UploadProps{
	action: string;
	defaultFileList?: UploadFile[];
	beforeUpload?: (file: File) => boolean | Promise<File>;// 上传前我们要做两件事:1.验证2.对文件进行转换,所以返回值为Boolean或promise
	onProgress?: (percentage: number,file: File) => void;
	onSuccess?: (data: any, file: File) => void;
	onError?: (err: any, file: File) => void;
	onChange?: (file: File) => void;
	onRemove?: (file: UploadFile) => void;
}
export const Upload: FC<UploadProps> = (props) => {
	const {action, defaultFileList, onProgress, onSuccess, onError, onRemove} = props;
	const fileInput = useRef<HTMInputElement>(null)
	const [ fileList, setFileList ] = useState<UploadFile[]>(defaultFileList || [])
	const handleClick = () => {
		if(fileInput.current){
			fileInput.current.click();
		}
	}
	// 值变化时选择文件
	const handleFileChange = (e:ChangeEvent<HTMLInputElement>) => {
		const file = e.target.files;
		if(!files) return;
		uploadFiles(files);
		if(fileInput.current){
			fileInput.current.value = '';
		}
	}
	const handleRemove = (file: UploadFile) => {
    	setFileList((prevList) => {
      		return prevList.filter(item => item.uid !== file.uid)
    	})
    	if (onRemove) {
      		onRemove(file)
    	}
  	}
	// FileList是一个类数组对象,想在类数组上使用数组的方法,需要先将类数组转换为数组
	const uploadFiles = (files: FileList) => {
		let postFiles = Array.from(files)
		postFiles.forEach(file => {
			if(!beforeUpload){// 没有beforeUpload,直接上传
				post(file)
			}else{
				const result = beforeUpload(file)
				if(result && result instanceof Promise){
					result.then(res => post(res))
				}else if(result !== false) {
					post(result)
				}
			}
		})
	}
	const post = (file: File) => {
		const formData = new FormData()
		formData.append(file.name,file);
		axios.post(action, formData,{
			headers:{
				'Content-Type': 'multipart/form-data'
			}
			//此时我们需要拿到onProgress事件,axios给我们提供了一个onUploadProgress来显示上传进度
			onUploadProgress: (e) => {
				let percentage = Math.round((e.loaded * 100) / e.total) || 0;
				if(percentage < 100) {
					if(onProgress) {
						onProgress(percentage, file)
					}
				}
			}
		}).then(res => {
			console.log(res)
			if(onSuccess){
				onSuccess(res.data, file)//第一个参数是服务器返回的数据,第二个参数是file对象
			}
			if(onChange) {
				onChange(file)
			}
		}).catch(err => {
			console.error(err)
			if(onError){
				onError(err, file)//第一个参数是err对象,第二个参数是file对象
			}
			if(onChange) {
				onChange(file)
			}
		})
	}

	return (
		<div className='viking-upload-component'>
			<button btnType='primary' onClick={handleClick}}>Upload File</button>
			<input 
				className='viking-file-input' 
				style={{display: 'none'}} 
				ref={fileInput} 
				onChang={handleFileChange}
				type='file' />
			<UploadList 
        		fileList={fileList}
        		onRemove={handleRemove}
      		/>
		</div>
	)
}

进度条

根据传入的百分比显示一个进度条,
在这里插入图片描述
最外面有个灰色的条,高度可配置,使用相对定位:position:relative;然后右边是个进度条,颜色可配置,使用绝对定位:position:absolute;,让他浮在灰色的条上面去,还可以配置文字是否显示,如果显示,里面的字体垂直居中。新建src/components/Progress/progress.tsx

import React, { FC } from 'react'
import { ThemeProps } from '../Icon/icon'
export interface ProgressProps {
  percent: number;// 显示加载到百分之几
  strokeHeight?: number;// 灰色的条的高度
  showText?: boolean;// 是否展示里面的字
  styles?: React.CSSProperties;
  theme?: ThemeProps;
}

const Progress: FC<ProgressProps> = (props) => {
  const {
    percent,
    strokeHeight,
    showText,
    styles,
    theme,
  } = props
  return (
    <div className="viking-progress-bar" style={styles}>
      <div className="viking-progress-bar-outer" style={{ height: `${strokeHeight}px`}}>
        <div 
          className={`viking-progress-bar-inner color-${theme}`}
          style={{width: `${percent}%`}}
        >
          {showText && <span className="inner-text">{`${percent}%`}</span>}
        </div>
      </div>
    </div>
  )
}

Progress.defaultProps = {
  strokeHeight: 15,
  showText: true,
  theme: "primary",
}
export default Progress;

在src/components/Upload/uploadList.tsx中使用

return (
	<li className="viking-upload-list-item" key={item.uid}>
    	<span className={`file-name file-name-${item.status}`}>
    		<Icon icon="file-alt" theme="secondary" />
        	{item.name}
    	</span>
    	<span className="file-status">
    		{(item.status === 'uploading' || item.status === 'ready') && <Icon icon="spinner" spin theme="primary" />}
        	{item.status === 'success' && <Icon icon="check-circle" theme="success" />}
        	{item.status === 'error' && <Icon icon="times-circle" theme="danger" />}
		</span>
    	<span className="file-actions">
    		<Icon icon="times" onClick={() => { onRemove(item)}}/>
    	</span>
    	// 使用
    	{item.status === 'uploading' && <Progress percent={item.percent || 0} />}
	</li>
)

丰富上传数据

我们的upload组件发送http的post请求时,可定制性非常差,在发送请求时,有几个属性非常重要,用户有可能会自定义,如:自定义header,自定义name属性(发送到后台的文件参数名称),自定义data属性(上传所需的额外参数),添加input本身的file约束属性(multiple、accept等)。用户界面和交互功能也需要完善,如自定义触发的元素,支持推拽上传,点击上传文件名称,添加onPreivew事件(点击文件展示文件,点击文件名更改文件名等操作)
现在我们先实现【自定义header,自定义name属性(发送到后台的文件参数名称),自定义data属性(上传所需的额外参数),添加input本身的file约束属性(multiple、accept等)】

import React,{FC, useRef} from 'react'
import axios from 'axios'
import Button from '../Button/button'
import UploadList from './uploadList'
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;
}

export interface UploadProps{
	action: string;
	defaultFileList?: UploadFile[];
	beforeUpload?: (file: File) => boolean | Promise<File>;// 上传前我们要做两件事:1.验证2.对文件进行转换,所以返回值为Boolean或promise
	onProgress?: (percentage: number,file: File) => void;
	onSuccess?: (data: any, file: File) => void;
	onError?: (err: any, file: File) => void;
	onChange?: (file: File) => void;
	onRemove?: (file: UploadFile) => void;
	headers?: {[key: string]: any };
  	name?: string;
  	data?: {[key: string]: any };
  	withCredentials?: boolean;// 是否携带cookie
  	accept?: string;
  	multiple?: boolean;
}
export const Upload: FC<UploadProps> = (props) => {
	const {action, defaultFileList, onProgress, onSuccess, onError, onRemove, name, headers, data, withCredentials, accept, multiple,} = props;
	const fileInput = useRef<HTMInputElement>(null)
	const [ fileList, setFileList ] = useState<UploadFile[]>(defaultFileList || [])
	const handleClick = () => {
		if(fileInput.current){
			fileInput.current.click();
		}
	}
	// 值变化时选择文件
	const handleFileChange = (e:ChangeEvent<HTMLInputElement>) => {
		const file = e.target.files;
		if(!files) return;
		uploadFiles(files);
		if(fileInput.current){
			fileInput.current.value = '';
		}
	}
	const handleRemove = (file: UploadFile) => {
    	setFileList((prevList) => {
      		return prevList.filter(item => item.uid !== file.uid)
    	})
    	if (onRemove) {
      		onRemove(file)
    	}
  	}
	// FileList是一个类数组对象,想在类数组上使用数组的方法,需要先将类数组转换为数组
	const uploadFiles = (files: FileList) => {
		let postFiles = Array.from(files)
		postFiles.forEach(file => {
			if(!beforeUpload){// 没有beforeUpload,直接上传
				post(file)
			}else{
				const result = beforeUpload(file)
				if(result && result instanceof Promise){
					result.then(res => post(res))
				}else if(result !== false) {
					post(result)
				}
			}
		})
	}
	const post = (file: File) => {
		let _file: UploadFile = {
      		uid: Date.now() + 'upload-file',
      		status: 'ready',
      		name: file.name,
      		size: file.size,
      		percent: 0,
      		raw: file
    	}
    	//更新数据列表
    	//setFileList([_file, ...fileList])
    	//如果我们选中了多个文件,但是只有一个上传了,因为上一行代码中,fileList多次调用post时,fileList还没更新到最新值,
    	//所以最后就剩了一个file,所以还是需要使用回调函数的方式更新,将上面代码注释,改为下面这行:
    	setFileList(prevList => {
      		return [_file, ...prevList]
    	})
		const formData = new FormData()
		formData.append(name || 'file', file)
		//把data添加进formData一起上传
    	if (data) {
      		Object.keys(data).forEach(key => {
        		formData.append(key, data[key])
      		})
    	} 
		axios.post(action, formData,{
			headers:{
				...headers,// 添加自定义headers
				'Content-Type': 'multipart/form-data'
			}
			withCredentials,// 是否携带cookie
			//此时我们需要拿到onProgress事件,axios给我们提供了一个onUploadProgress来显示上传进度
			onUploadProgress: (e) => {
				let percentage = Math.round((e.loaded * 100) / e.total) || 0;
				if(percentage < 100) {
					updateFileList(_file, { percent: percentage, status: 'uploading'})
					if(onProgress) {
						onProgress(percentage, file)
					}
				}
			}
		}).then(res => {
			updateFileList(_file, {status: 'success', response: resp.data})
			console.log(res)
			if(onSuccess){
				onSuccess(res.data, file)//第一个参数是服务器返回的数据,第二个参数是file对象
			}
			if(onChange) {
				onChange(file)
			}
		}).catch(err => {
			updateFileList(_file, { status: 'error', error: err})
			console.error(err)
			if(onError){
				onError(err, file)//第一个参数是err对象,第二个参数是file对象
			}
			if(onChange) {
				onChange(file)
			}
		})
	}

	return (
		<div className='viking-upload-component'>
			<button btnType='primary' onClick={handleClick}}>Upload File</button>
			<input 
				className='viking-file-input' 
				style={{display: 'none'}} 
				ref={fileInput} 
				onChang={handleFileChange}
				type='file'
				accept={accept}
          		multiple={multiple} />
			<UploadList 
        		fileList={fileList}
        		onRemove={handleRemove}
      		/>
		</div>
	)
}

拖拽上传

当有文件被拖拽到区域时,触发DragOver,给区域添加一个class,增强区域效果,接下来,若文件被拖出区域,删除特定的class,区域变回初始状态,所以这里需要一个state来管理class的添加与删除,若你拖拽到该区域,然后松开了鼠标,文件就被drag进去了,此时触发onDrag事件,我们能从该事件对象中拿到被drag的文件,他是个FileList,然后我们将该文件传到外面去,让upload事件完成整个上传的工作。流程图如下:
在这里插入图片描述
通过一个属性控制是否开启拖拽上传,新建src/components/Upload/dragger.tsx。

import React, { FC, useState, DragEvent } from 'react'
import classNames from 'classnames'

interface DraggerProps {
  //我们需要一个事件,来告诉我们被drag的文件
  onFile: (files: FileList) => void;
}

export const Dragger: FC<DraggerProps> = (props) => {
  const { onFile, children } = props
  //文件是否被拖拽到区域
  const [ dragOver, setDragOver ] = useState(false)
  const klass = classNames('viking-uploader-dragger', {
    'is-dragover': dragOver// 如果文件被拖拽到区域,添加is-dragover类名
  })
  const handleDrop = (e: DragEvent<HTMLElement>) => {
    e.preventDefault()//先禁止掉默认事件
    setDragOver(false)//设置变量,文件是否被拖拽到区域
    //e.dataTransfer.files可以拿到fileList
    onFile(e.dataTransfer.files)
  }
  const handleDrag = (e: DragEvent<HTMLElement>, over: boolean) => {
    e.preventDefault()//先禁止掉默认事件
    setDragOver(over)//设置变量,文件是否被拖拽到区域
  }
  //ondragover 事件在可拖动元素或选取的文本正在拖动到放置目标时触发。
  //ondragleave 事件在可拖动的元素或选取的文本移出放置目标时执触发。
  //ondrop 事件在可拖动元素或选取的文本放置在目标区域时触发。
  return (
    <div 
      className={klass}
      onDragOver={e => { handleDrag(e, true)}}
      onDragLeave={e => { handleDrag(e, false)}}
      onDrop={handleDrop}
    >
      {children}
    </div>
  )
}

export default Dragger;

在upload.tsx中使用,src/components/Upload/upload.tsx完整代码:

import React, { FC, useRef, ChangeEvent, useState } from 'react'
import axios from 'axios'
import UploadList from './uploadList'
import Dragger from './dragger'
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;
}
export interface UploadProps {
  action: string;
  defaultFileList?: UploadFile[];
  beforeUpload? : (file: File) => boolean | Promise<File>;
  onProgress?: (percentage: number, file: File) => void;
  onSuccess?: (data: any, file: File) => void;
  onError?: (err: any, file: File) => void;
  onChange?: (file: File) => void;
  onRemove?: (file: UploadFile) => void;

  headers?: {[key: string]: any };
  name?: string;
  data?: {[key: string]: any };
  withCredentials?: boolean;// 是否携带cookie
  accept?: string;
  multiple?: boolean;
  drag?: boolean;// 是否拖拽上传
}

export const Upload: FC<UploadProps> = (props) => {
  const {
    action,
    defaultFileList,
    beforeUpload,
    onProgress,
    onSuccess,
    onError,
    onChange,
    onRemove,
    name,
    headers,
    data,
    withCredentials,
    accept,
    multiple,
    children,
    drag,
  } = props
  const fileInput = useRef<HTMLInputElement>(null)
  const [ fileList, setFileList ] = useState<UploadFile[]>(defaultFileList || [])
  // 更新列表的某一项(updateFile项),
  const updateFileList = (updateFile: UploadFile, updateObj: Partial<UploadFile>) => {
    // 上传过程中更改状态。setFileList不仅可以接收新值,也可以接收函数,函数的参数就是当前的值
    setFileList(prevList => {
      return prevList.map(file => {
        if (file.uid === updateFile.uid) {
          //用我们要更新的属性值,覆盖该项的属性值
          return { ...file, ...updateObj }
        } else {
          return file
        }
      })
    })
  }
  const handleClick = () => {
    if (fileInput.current) {
      fileInput.current.click()
    }
  }
  const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files
    if(!files) {
      return
    }
    uploadFiles(files)
    if (fileInput.current) {
      fileInput.current.value = ''
    }
  }
  const handleRemove = (file: UploadFile) => {
    setFileList((prevList) => {
      return prevList.filter(item => item.uid !== file.uid)
    })
    if (onRemove) {
      onRemove(file)
    }
  }
  const uploadFiles = (files: FileList) => {
    let postFiles = Array.from(files)
    postFiles.forEach(file => {
      if (!beforeUpload) {
        post(file)
      } else {
        const result = beforeUpload(file)
        if (result && result instanceof Promise) {
          result.then(processedFile => {
            post(processedFile)
          })
        } else if (result !== false) {
          post(file)
        }
      }
    })
  }
  const post = (file: File) => {
    let _file: UploadFile = {
      uid: Date.now() + 'upload-file',
      status: 'ready',
      name: file.name,
      size: file.size,
      percent: 0,
      raw: file
    }
    //更新数据列表
    //setFileList([_file, ...fileList])
    //如果我们选中了多个文件,但是只有一个上传了,因为上一行代码中,fileList多次调用post时,fileList还没更新到最新值,
    //所以最后就剩了一个file,所以还是需要使用回调函数的方式更新,将上面代码注释,改为下面这行:
    setFileList(prevList => {
      return [_file, ...prevList]
    })
    const formData = new FormData()
    formData.append(name || 'file', file)
    //把data添加进formData一起上传
    if (data) {
      Object.keys(data).forEach(key => {
        formData.append(key, data[key])
      })
    } 
    axios.post(action, formData, {
      headers: {
        ...headers,// 添加自定义headers
        'Content-Type': 'multipart/form-data'
      },
      withCredentials,// 是否携带cookie
      onUploadProgress: (e) => {
        let percentage = Math.round((e.loaded * 100) / e.total) || 0;
        if (percentage < 100) {
          updateFileList(_file, { percent: percentage, status: 'uploading'})
          if (onProgress) {
            onProgress(percentage, file)
          }
        }
      }
    }).then(resp => {
      updateFileList(_file, {status: 'success', response: resp.data})
      if (onSuccess) {
        onSuccess(resp.data, file)
      }
      if (onChange) {
        onChange(file)
      }
    }).catch(err => {
      updateFileList(_file, { status: 'error', error: err})
      if (onError) {
        onError(err, file)
      }
      if (onChange) {
        onChange(file)
      }
    })
  }

  return (
    <div 
      className="viking-upload-component"
    >
      <div className="viking-upload-input"
        style={{display: 'inline-block'}}
        onClick={handleClick}>
          {/*
          把固定的触发元素改为children传入,一开始我们这里使用的是button,现在使用children代替
          如果drag为true,允许拖拽上传,那么就是用drag组件,否则之间展示children
          */}
          {drag ? 
            <Dragger onFile={(files) => {uploadFiles(files)}}>
              {children}
            </Dragger>:
            children
          }
        <input
          className="viking-file-input"
          style={{display: 'none'}}
          ref={fileInput}
          onChange={handleFileChange}
          type="file"
          accept={accept}
          multiple={multiple}
        />
      </div>

      <UploadList 
        fileList={fileList}
        onRemove={handleRemove}
      />
    </div>
  )
}

Upload.defaultProps = {
  name: 'file'
}
export default Upload;
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Element UIUpload 组件是一个文件上传组件,允许用户上传多个文件,并支持拖拽上传和文件预览。下面我将详细介绍这个组件的使用方法。 ## 安装 首先,需要在项目中安装 Element UI。 ```bash npm install element-ui --save ``` 然后,在 main.js 中引入 Element UI。 ```javascript import Vue from 'vue' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' Vue.use(ElementUI) ``` ## 基本用法 在需要使用 Upload组件中,可以这样写: ```html <el-upload class="upload-demo" action="/upload" :data="{ user_id: 123 }" :on-success="handleSuccess" :on-error="handleError" :before-upload="beforeUpload" :file-list="fileList" :auto-upload="false"> <el-button slot="trigger" size="small" type="primary">选取文件</el-button> <el-button size="small" type="success" @click="submitUpload">上传到服务器</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> </el-upload> ``` 上面代码中,`action` 属性是上传的后端接口地址,`data` 属性是上传时需要携带的额外参数,`on-success` 和 `on-error` 属性分别是上传成功和失败时的回调函数,`before-upload` 属性是上传前的校验函数,`file-list` 属性是已经上传的文件列表,`auto-upload` 属性表示是否自动上传。 在 Upload 组件中,需要通过 `slot` 分别定义两个按钮,分别是选取文件和上传到服务器的按钮。同时,可以通过 `slot` 定义提示信息。 ```html <el-button slot="trigger" size="small" type="primary">选取文件</el-button> <el-button size="small" type="success" @click="submitUpload">上传到服务器</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> ``` 最后,需要在 Vue 实例中定义对应的函数。 ```javascript export default { data() { return { fileList: [] } }, methods: { handleSuccess(response, file, fileList) { console.log(response, file, fileList); }, handleError(error, file, fileList) { console.log(error, file, fileList); }, beforeUpload(file) { const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'; const isLt2M = file.size / 1024 / 1024 < 2; if (!isJPG) { this.$message.error('上传头像图片只能是 JPG 格式!'); } if (!isLt2M) { this.$message.error('上传头像图片大小不能超过 2MB!'); } return isJPG && isLt2M; }, submitUpload() { this.$refs.upload.submit(); } } } ``` 上面代码中,`handleSuccess` 和 `handleError` 分别是上传成功和失败时的回调函数,在这里可以对上传的结果进行处理。`beforeUpload` 是上传前的校验函数,可以在这里对上传的文件进行校验。`submitUpload` 用于手动触发上传。 ## 高级用法 ### 限制上传文件类型和大小 可以通过 `accept` 和 `before-upload` 属性来限制上传文件的类型和大小。 ```html <el-upload class="upload-demo" action="/upload" :data="{ user_id: 123 }" :on-success="handleSuccess" :on-error="handleError" :before-upload="beforeUpload" :file-list="fileList" :auto-upload="false" accept="image/*" :limit="3" :on-exceed="handleExceed"> <el-button slot="trigger" size="small" type="primary">选取文件</el-button> <el-button size="small" type="success" @click="submitUpload">上传到服务器</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> </el-upload> ``` 上面代码中,`accept` 属性限制了只能上传图片类型的文件,`before-upload` 函数限制了文件大小不超过 500KB,同时还设置了最多上传 3 个文件的限制,并在超出限制时触发 `on-exceed` 方法。 ```javascript handleExceed(files, fileList) { this.$message.warning(`当前限制选择 ${this.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`); } ``` ### 上传到阿里云 OSS 可以通过 `before-upload` 和 `custom-request` 属性来实现上传到阿里云 OSS。 ```html <el-upload class="upload-demo" :action="ossConfig.host" :data="ossConfig.params" :on-success="handleSuccess" :on-error="handleError" :before-upload="beforeUpload" :file-list="fileList" :auto-upload="false" :custom-request="ossConfig.customRequest"> <el-button slot="trigger" size="small" type="primary">选取文件</el-button> <el-button size="small" type="success" @click="submitUpload">上传到服务器</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> </el-upload> ``` 上面代码中,`action` 属性设置为阿里云 OSS 的上传地址,`data` 属性设置为上传时需要携带的额外参数。在 `before-upload` 函数中,需要返回一个 Promise 对象,该 Promise 对象中需要实现上传到阿里云 OSS 的逻辑。 ```javascript beforeUpload(file) { const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'; const isLt2M = file.size / 1024 / 1024 < 2; if (!isJPG) { this.$message.error('上传头像图片只能是 JPG 格式!'); } if (!isLt2M) { this.$message.error('上传头像图片大小不能超过 2MB!'); } return isJPG && isLt2M && new Promise((resolve, reject) => { const ossConfig = this.getOssConfig(); this.ossConfig = ossConfig; const client = new OSS({ accessKeyId: ossConfig.accessid, accessKeySecret: ossConfig.accesskey, stsToken: ossConfig.securitytoken, bucket: ossConfig.bucket, region: ossConfig.region, cname: true }); client.multipartUpload(ossConfig.dir + '/' + file.name, file).then((result) => { console.log(result); resolve(); }).catch((error) => { console.log(error); reject(); }); }); }, getOssConfig() { // 获取阿里云 OSS 的配置 } ``` 在 `custom-request` 函数中,可以实现上传成功和失败的回调函数。 ```javascript ossConfig: { host: '', params: {}, customRequest: (config) => { const { action, data, file, headers, onError, onSuccess, onProgress } = config; const xhr = new XMLHttpRequest(); xhr.open('POST', action, true); Object.keys(headers).forEach((key) => { xhr.setRequestHeader(key, headers[key]); }); xhr.onload = function onload() { if (xhr.readyState === 4 && xhr.status === 200 && xhr.responseText !== '') { try { const response = JSON.parse(xhr.responseText); onSuccess(response, xhr); } catch (error) { onError(error, xhr); } } else { onError(new Error('上传失败'), xhr); } }; xhr.onerror = function onerror(error) { onError(error, xhr); }; xhr.upload.onprogress = function onprogress(event) { if (event.total > 0) { event.percent = (event.loaded / event.total) * 100; } onProgress(event, xhr); }; const formData = new FormData(); Object.keys(data).forEach((key) => { formData.append(key, data[key]); }); formData.append('file', file); xhr.send(formData); } }, ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值