拖拽上传与多文件上传集成方案(Vue3 + TypeScript)
- 拖拽上传支持:允许用户通过拖拽文件到指定区域进行上传。
- 多文件上传支持:支持同时上传多个文件。
- 并发控制:限制同时上传的文件数量。
- 进度更新:实时显示每个文件的上传进度。
- 状态管理:管理文件的上传状态(待上传、上传中、已完成、错误)。
- 错误处理:处理上传过程中的错误,并提供相应的反馈。
一、核心功能实现
-
拖拽区域交互设计
<template>
<div id="upload-area" class="upload-area" @dragover.prevent @drop.prevent="handleDrop">
<p>拖拽文件到此处或点击选择文件</p>
<input type="file" id="file-input" webkitdirectory multiple hidden @change="handleFileInput" />
</div>
<div id="file-list" class="file-list">
<div v-for="fileItem in fileList" :key="fileItem.id" class="file-item">
<span>{{ fileItem.file.name }}</span>
<div class="progress">
<div class="progress-bar" :style="{ width: fileItem.progress + '%' }"></div>
</div>
<span>{{ fileItem.status }}</span>
</div>
</div>
</template>
- 使用
@dragover.prevent
阻止浏览器默认拖拽行为 - 同时支持
multiple
多选文件和webkitdirectory
文件夹选择拖拽事件处理
2.拖拽事件处理
这段代码主要用于处理文件拖放事件(DragEvent
),并递归地读取拖放的文件和文件夹中的所有文件,最后将这些文件上传
- 获取拖放的文件项:从
DragEvent
对象中获取文件项集合。 - 初始化文件数组:用于存储所有读取到的文件。
- 定义递归遍历函数:遍历文件和文件夹,读取文件并添加到文件数组中。
- 遍历拖放的文件项:将每个文件项转换为
FileSystemEntry
对象,并调用递归遍历函数处理。 - 上传文件:调用
uploadFiles
函数上传所有读取到的文件。
const handleDrop = (e: DragEvent) => {
//获取拖放事件中的文件项集合
const items = e.dataTransfer?.items;
if (!items) return;
const files: File[] = [];
const traverseEntries = async (entry: any) => {
if (entry.isFile) {
//如果当前项是文件,调用 getFileFromEntry 获取文件并添加到 files 数组中。
const file = await getFileFromEntry(entry);
files.push(file);
} else if (entry.isDirectory) {
//如果当前项是文件夹,创建一个 DirectoryReader 对象,读取文件夹中的所有项,并递归调用 traverseEntries 处理每个项。
const reader = entry.createReader();
const entries = await readAllEntries(reader);
await Promise.all(entries.map(traverseEntries));
}
};
Array.from(items).forEach(item => {
//获取文件项对应的 FileSystemEntry 对象。
const entry = item.webkitGetAsEntry();
if (entry) traverseEntries(entry);
});
addFiles(files);
};
const getFileFromEntry = (entry: any): Promise<File> => {
return new Promise((resolve, reject) => {
entry.file((file: File) => {
resolve(file);
}, reject);
});
};
//readAllEntries:读取 DirectoryReader 对象中的所有项。
//reader.readEntries:异步读取文件夹中的项,直到读取完毕。
//entries.push(...batch):将读取到的项添加到 entries 数组中。
//resolve(entries):读取完毕后,通过 Promise 返回所有项。
const readAllEntries = (reader: any): Promise<any[]> => {
return new Promise((resolve, reject) => {
const entries: any[] = [];
const readEntries = () => {
reader.readEntries((batch: any[]) => {
if (batch.length === 0) {
resolve(entries);
} else {
entries.push(...batch);
readEntries();
}
}, reject);
};
readEntries();
});
};
二、多文件上传管理
-
文件列表状态维护
//id: 文件的唯一标识符,通常由文件内容生成的哈希值表示,确保每个文件在列表中是唯一的。
//file表示文件对象,包含了文件的基本信息(如名称、大小、类型等)。
//progress表示文件上传的进度,范围为 0 到 100 的数字。
//status:表示文件的上传状态,有以下几种可能:
//pending':等待上传。
//uploading':正在上传。
//done':上传完成。
//error':上传失败
interface FileItem {
id: string;
file: File;
progress: number;
status: 'pending' | 'uploading' | 'done' | 'error';
}
const fileList = ref<FileItem[]>([]);
const handleFileInput = (e) => {
const files = e.target.files;
addFiles(files);
};
const addFiles = (newFiles: File[]) => {
newFiles.forEach(file => {
//过滤掉已经上传过的文件
if (!fileList.value.some(f => f.id === generateFileHash(file))) {
fileList.value.push({
id: generateFileHash(file),
file,
progress: 0,
status: 'pending'
});
}
});
};
//读取文件内容并生成其 SHA-256 哈希值,作为文件的唯一标识符。
const generateFileHash = (file: File): string => {
const reader = new FileReader();
return new Promise<string>((resolve, reject) => {
reader.onload = (e) => {
const arrayBuffer = e.target?.result as ArrayBuffer;
const hash = crypto.createHash('sha256').update(arrayBuffer).digest('hex');
resolve(hash);
};
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
};
2.并发上传控制
//定义了允许的最大并发上传数为 5。超出此限制时,等待已有任务完成后再继续。
const MAX_CONCURRENT = 5;
//使用 Vue 的 ref 创建一个响应式数组,用于存储当前正在进行的上传任务(每个任务是一个 Promise)。
const uploadPool = ref<Promise<any>[]>([]);
const startUpload = async () => {
const pendingFiles = fileList.value.filter(f => f.status === 'pending');
for (const fileItem of pendingFiles) {
//如果当前上传任务数量达到最大并发限制(MAX_CONCURRENT),则等待任意一个任务完成后再继续。
if (uploadPool.value.length >= MAX_CONCURRENT) {
await Promise.race(uploadPool.value);
}
const promise = axios.post('/upload', createFormData(fileItem), {
onUploadProgress: (e) => {
fileItem.progress = Math.round((e.loaded / e.total) * 100);
}
}).finally(() => {
//无论上传成功或失败,都会执行 finally 回调,从 uploadPool 中移除该任务。
uploadPool.value.splice(uploadPool.value.indexOf(promise), 1);
});
//uploadPool.value.push(promise)
//将当前上传任务的 Promise 添加到 uploadPool 中。
//fileItem.status = 'uploading'
//更新文件状态为 'uploading',表示该文件正在上传。
uploadPool.value.push(promise);
fileItem.status = 'uploading';
}
};
const createFormData = (file) => {
const formData = new FormData();
formData.append('file', file);
return formData;
};