最近业务中遇到有关上传文件的需求,其中有涉及到支持同时上传文件和文件夹的功能,Input[type=file]要么只上传文件,要么加webkitdirectory只上传文件夹,没办法同时上传文件和文件夹,后面了解到可以通过拖拽实现同时将文件和文件夹拉到捕获区获取文件.
实现结果
可以点击上传文件,或者点击上传文件夹,但只能二选其一,如果是拖拽,可以将文件和文件夹同时拖拽进来。

点击上传
这里我有用到vue3的一些api,需要的可以结合自己的技术栈修改一下。
<template> <div class="c-fileUpload-container" ref="fileUploadRef" @click="handleFileClick"> <input type="file" style="display: none" :multiple="multiple" :accept="accept" ref="fileInputRef" /> <input type="file" webkitdirectory mozdirectory odirectory style="display: none" ref="directoryInputRef" /> <div class="c-fileUpload-container-desc"> <slot> <p class="c-fileUpload-drag-icon"> <cloud-upload-outlined /> </p> <p class="c-fileUpload-text"> {{ noDragDropSupport ? '浏览器不支持拖拽上传,' : `将文件${directory ? '/文件夹' : ''} 拖到此处,或 ` }} <a @click.stop="handleFileClick">点击上传文件</a> <template v-if="directory"> 或 <a style="z-index: 3" @click.stop="handleDirectoryClick">点击上传文件夹</a> </template> </p> </slot> </div> </div> </template>
|
js:
const handleFileChange = e => { // 获取文件 const files = e.target.files // 遍历文件,单个文件调用handleFile方法 Array.from(files).map(file => { // 处理文件 }) } const handleDirectoryChange = e => { // 获取文件夹 // 遍历文件,单个文件调用handleFile方法 const files = e.target.files Array.from(files).map(file => { // 处理文件 }) } fileInputRef.value.addEventListener('change', handleFileChange) directoryInputRef.value.addEventListener('change', handleDirectoryChange) const handleFileClick = () => { fileInputRef.value.click() } const handleDirectoryClick = () => { directoryInputRef.value.click() }
|
webkitdirectory mozdirectory odirectory 为了兼容性选择文件夹
webkitdirectory为chrome
mozdirectory为firefox
odirectory为opera
拖拽同时上传文件和文件夹
兼容性检查
function checkDragDropSupport() { const div = document.createElement('div'); return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div); } const noDragDropSupport = !checkDragDropSupport() if(!noDragDropSupport){ }
|
拖拽过程样式
可以通过监听dragover/dragEnter和dragLeave实时改变拖拽区域的样式,增强用户交互效果,如改变border等
const handleDragLeave = e => { e.preventDefault() e.stopPropagation() fileUploadRef.value.style.border = '1px dashed #d9d9d9' } const handleDragOver = e => { e.preventDefault() e.stopPropagation() fileUploadRef.value.style.border = '1px dashed #6bbcff' } fileUploadRef.value.addEventListener('dragleave', handleDragLeave) fileUploadRef.value.addEventListener('dragover', handleDragOver)
|
effectAllowed和 dropEffect
可以通过调整拖拽释放区的这两个效果来控制文件进去之后的鼠标样式,默认为copy
dropEffect 表示拖放操作的视觉效果,effectAllowed 用来指定当元素被拖放式所允许的视觉效果
dropeffect可取值:none|copy|link|move
effectAllowed可取值:copy|move|link|copyLink|copyMove|linkMove|all|none|uninitialized
获取文件
这是重中之重,通过onDrop事件,我们可以获取到dataTransfer获取到相关的文件和文件夹,但此时他们不是File对象,我们需要转换,及递归将文件夹中的文件获取。
const handleDrop = e => { e.preventDefault() e.stopPropagation() fileUploadRef.value.style.border = '1px dashed #d9d9d9' getFilesFromDataTransferItemList(e.dataTransfer.items) } fileUploadRef.value.addEventListener('drop', handleDrop)
const getFilesFromDataTransferItemList = items => { const dfsForDirectory = async item => { if (item.isFile) { const file = await readFileEntrieQueue(item) emit('handleFile', file) } else { const entries = await readDirEntrieQueue(item) for (let entry of entries) { dfsForDirectory(entry) } } } for (let i = 0; i < items.length; i++) { dfsForDirectory(items[i].webkitGetAsEntry()) } } const readDirEntrieQueue = createQueue(20, entery => { return new Promise((resolve, reject) => { entery.createReader().readEntries(entries => { resolve(entries) }) }) }) const readFileEntrieQueue = createQueue(20, entery => { return new Promise((resolve, reject) => { entery.file(file => { resolve(file) }) }) })
|
这里我用了队列来控制读取文件和文件夹内容的任务,因为业务里需要同时拖拽几百个文件,如果一下子全部读取会造成卡顿,所以这里最好用队列控制一下。
分享一下我的队列函数
export const createQueue = (concurrency, fn) => { const queue = [] const runningQueue = []
const removeQueue = task => { const index = queue.findIndex(item => item === task) if (index !== -1) { console.log('取消排队') queue.splice(index, 1) } } const process = (dataItem, getRemoveQueue) => { return new Promise((resolve, reject) => { const run = async () => { if (runningQueue.length >= concurrency) { queue.push(run) getRemoveQueue && getRemoveQueue(() => removeQueue(run)) return } runningQueue.push(run) try { const result = await fn(dataItem) resolve(result) } catch (e) { reject(e) } finally { runningQueue.splice(runningQueue.indexOf(run), 1) if (queue.length) { queue.shift()() } } } run() }) } return process }
|
如有不妥,多多指教!