Vue3前端发送多个文件的方法详解
作者:BillKu
这篇文章主要介绍了基于Vue3 Composition API实现多文件上传组件,支持拖放和文件选择,包含进度条、状态显示、删除、重试等功能,响应式设计与模拟上传效果提升用户体验,需要的朋友可以参考下
我将为你创建一个美观且功能完整的多文件上传组件,支持拖放上传、文件列表显示和进度条展示。
设计思路
- 使用Vue3的Composition API
- 实现拖放上传和传统文件选择两种方式
- 显示上传进度和文件信息
- 提供删除和重新上传功能
- 使用模拟上传效果展示进度
下面是完整的实现代码:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue3 多文件上传</title> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="external nofollow" > <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); min-height: 100vh; display: flex; justify-content: center; align-items: center; padding: 20px; } .container { width: 100%; max-width: 800px; background: white; border-radius: 16px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); overflow: hidden; } header { background: #4a6bdf; color: white; padding: 20px; text-align: center; } h1 { font-size: 1.8rem; margin-bottom: 8px; } .subtitle { font-size: 1rem; opacity: 0.9; } .upload-container { padding: 30px; } .upload-areas { display: flex; gap: 20px; margin-bottom: 30px; } @media (max-width: 650px) { .upload-areas { flex-direction: column; } } .drag-drop-area { flex: 1; border: 2px dashed #4a6bdf; border-radius: 12px; padding: 30px; text-align: center; cursor: pointer; transition: all 0.3s ease; } .drag-drop-area:hover, .drag-drop-area.dragover { background: #f0f4ff; transform: translateY(-2px); } .drag-icon { font-size: 48px; color: #4a6bdf; margin-bottom: 15px; } .file-input-area { flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 15px; } .select-button { background: #4a6bdf; color: white; padding: 12px 24px; border-radius: 8px; cursor: pointer; transition: background 0.3s; display: inline-block; } .select-button:hover { background: #3a5bc7; } input[type="file"] { display: none; } .file-list { margin-top: 30px; } .file-list-title { font-size: 1.2rem; margin-bottom: 15px; color: #333; padding-bottom: 8px; border-bottom: 1px solid #eee; } .file-item { display: flex; align-items: center; padding: 12px 15px; border-radius: 8px; margin-bottom: 10px; background: #f9fafc; transition: transform 0.2s; } .file-item:hover { transform: translateX(5px); background: #f0f4ff; } .file-icon { color: #4a6bdf; font-size: 24px; margin-right: 15px; } .file-info { flex: 1; } .file-name { font-weight: 500; margin-bottom: 5px; word-break: break-word; } .file-size { font-size: 0.8rem; color: #777; } .file-progress { width: 100%; height: 6px; background: #e0e0e0; border-radius: 3px; margin-top: 8px; overflow: hidden; } .progress-bar { height: 100%; background: linear-gradient(90deg, #4a6bdf, #6f42c1); border-radius: 3px; transition: width 0.3s ease; } .file-actions { display: flex; gap: 10px; margin-left: 15px; } .action-btn { background: none; border: none; color: #777; cursor: pointer; font-size: 16px; transition: color 0.3s; } .action-btn:hover { color: #4a6bdf; } .delete-btn:hover { color: #e74c3c; } .upload-status { font-size: 0.8rem; margin-top: 5px; color: #4a6bdf; } .success { color: #2ecc71; } .error { color: #e74c3c; } .empty-state { text-align: center; padding: 30px; color: #777; } .empty-icon { font-size: 48px; margin-bottom: 15px; color: #ccc; } </style> </head> <body> <div id="app"> <div class="container"> <header> <h1>Vue3 多文件上传</h1> <div class="subtitle">支持拖放和选择文件,实时显示上传进度</div> </header> <div class="upload-container"> <div class="upload-areas"> <div class="drag-drop-area" @click="triggerFileInput" @drop.prevent="onDrop" @dragover.prevent="dragover = true" @dragleave="dragover = false" :class="{ 'dragover': dragover }" > <div class="drag-icon"> <i class="fas fa-cloud-upload-alt"></i> </div> <h3>拖放文件到此处</h3> <p>或者点击选择文件</p> </div> <div class="file-input-area"> <p>选择多个文件进行上传</p> <label class="select-button"> <i class="fas fa-folder-open"></i> 选择文件 <input type="file" multiple @change="onFileSelected" ref="fileInput" > </label> <p>最大支持10个文件同时上传</p> </div> </div> <div class="file-list"> <h3 class="file-list-title">文件列表</h3> <div v-if="files.length === 0" class="empty-state"> <div class="empty-icon"> <i class="fas fa-file-import"></i> </div> <p>尚未选择任何文件</p> </div> <div v-else> <div v-for="(file, index) in files" :key="index" class="file-item"> <div class="file-icon"> <i class="fas fa-file"></i> </div> <div class="file-info"> <div class="file-name">{{ file.name }}</div> <div class="file-size">{{ formatFileSize(file.size) }}</div> <div class="file-progress"> <div class="progress-bar" :style="{ width: file.progress + '%' }"></div> </div> <div class="upload-status" :class="file.status"> {{ getStatusText(file) }} </div> </div> <div class="file-actions"> <button v-if="file.status === 'error'" class="action-btn" @click="retryUpload(file)" title="重新上传" > <i class="fas fa-redo"></i> </button> <button class="action-btn delete-btn" @click="removeFile(index)" title="删除文件" > <i class="fas fa-trash"></i> </button> </div> </div> </div> </div> </div> </div> </div> <script> const { createApp, ref } = Vue; createApp({ setup() { const files = ref([]); const dragover = ref(false); const fileInput = ref(null); const triggerFileInput = () => { fileInput.value.click(); }; const onFileSelected = (event) => { const selectedFiles = Array.from(event.target.files); addFiles(selectedFiles); // 清空input,允许重复选择相同文件 event.target.value = ''; }; const onDrop = (event) => { dragover.value = false; const droppedFiles = Array.from(event.dataTransfer.files); addFiles(droppedFiles); }; const addFiles = (newFiles) => { // 限制最多10个文件 if (files.value.length + newFiles.length > 10) { alert('最多只能上传10个文件'); newFiles = newFiles.slice(0, 10 - files.value.length); } newFiles.forEach(file => { files.value.push({ file: file, name: file.name, size: file.size, progress: 0, status: 'pending' // pending, uploading, success, error }); }); // 开始上传所有新添加的文件 newFiles.forEach((_, index) => { const actualIndex = files.value.length - newFiles.length + index; simulateUpload(actualIndex); }); }; const simulateUpload = (index) => { files.value[index].status = 'uploading'; // 模拟上传进度 const interval = setInterval(() => { if (files.value[index].progress < 95) { files.value[index].progress += Math.floor(Math.random() * 10) + 5; if (files.value[index].progress > 95) { files.value[index].progress = 95; } } }, 300); // 模拟上传完成 setTimeout(() => { clearInterval(interval); // 随机决定上传成功或失败(实际应用中应根据真实上传结果) const isSuccess = Math.random() > 0.2; if (isSuccess) { files.value[index].progress = 100; files.value[index].status = 'success'; } else { files.value[index].status = 'error'; } }, 3000); }; const removeFile = (index) => { files.value.splice(index, 1); }; const retryUpload = (file) => { const index = files.value.indexOf(file); if (index !== -1) { files.value[index].progress = 0; files.value[index].status = 'pending'; simulateUpload(index); } }; const formatFileSize = (bytes) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const getStatusText = (file) => { switch (file.status) { case 'pending': return '等待上传'; case 'uploading': return `上传中... ${file.progress}%`; case 'success': return '上传成功'; case 'error': return '上传失败'; default: return ''; } }; return { files, dragover, fileInput, triggerFileInput, onFileSelected, onDrop, removeFile, retryUpload, formatFileSize, getStatusText }; } }).mount('#app'); </script> </body> </html>
功能说明
文件选择方式:
- 拖放文件到指定区域上传
- 点击"选择文件"按钮选择多个文件
文件列表显示:
- 显示文件名和大小
- 实时显示上传进度条
- 显示上传状态(等待、上传中、成功、失败)
操作功能:
- 删除已选择的文件
- 重新上传失败的文件
响应式设计:
- 在移动设备上自动调整布局
实际应用提示
在实际项目中,你需要将simulateUpload
函数替换为真实的上传逻辑,通常使用fetch
或axios
库来实现文件上传。真实上传代码可能类似于:
const realUpload = (fileItem) => { const formData = new FormData(); formData.append('file', fileItem.file); axios.post('/upload', formData, { onUploadProgress: (progressEvent) => { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); fileItem.progress = percentCompleted; } }) .then(response => { fileItem.status = 'success'; }) .catch(error => { fileItem.status = 'error'; }); };
这个实现提供了良好的用户体验和直观的界面,你可以根据需要进一步扩展功能。
Vue3 多文件上传技术点解析
下面我将详细梳理Vue3中实现多文件上传的各项技术点,并提供一个完整的示例。
核心实现技术点
1. Composition API 使用
Vue3的Composition API提供了更好的代码组织和复用能力
import { ref, reactive, computed } from 'vue' export default { setup() { // 响应式数据 const files = ref([]) const uploadProgress = reactive({}) const isUploading = ref(false) // 计算属性 const totalProgress = computed(() => { // 计算总上传进度 }) // 方法 const handleFileSelect = (event) => { // 处理文件选择 } return { files, uploadProgress, isUploading, totalProgress, handleFileSelect } } }
2. 文件选择与处理
使用input元素和File API处理文件选择
const handleFileSelect = (event) => { const selectedFiles = Array.from(event.target.files) // 验证文件类型和大小 const validFiles = selectedFiles.filter(file => { const maxSize = 10 * 1024 * 1024 // 10MB const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'] return file.size <= maxSize && allowedTypes.includes(file.type) }) // 添加到文件列表 files.value = [...files.value, ...validFiles] }
3. 拖放功能实现
使用HTML5拖放API
const handleDrop = (event) => { event.preventDefault() isDragging.value = false const droppedFiles = Array.from(event.dataTransfer.files) // 处理拖放的文件 } const handleDragOver = (event) => { event.preventDefault() isDragging.value = true } const handleDragLeave = (event) => { event.preventDefault() isDragging.value = false }
4. 分块上传与进度跟踪
对于大文件,使用分块上传可以提高可靠性和用户体验
const uploadFile = async (file) => { const chunkSize = 1024 * 1024 // 1MB const totalChunks = Math.ceil(file.size / chunkSize) for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { const start = chunkIndex * chunkSize const end = Math.min(start + chunkSize, file.size) const chunk = file.slice(start, end) try { const formData = new FormData() formData.append('file', chunk) formData.append('chunkIndex', chunkIndex) formData.append('totalChunks', totalChunks) formData.append('fileId', file.id) // 唯一文件标识 await axios.post('/upload-chunk', formData, { onUploadProgress: (progressEvent) => { // 更新上传进度 const chunkProgress = (progressEvent.loaded / progressEvent.total) * 100 updateFileProgress(file.id, chunkIndex, chunkProgress, totalChunks) } }) } catch (error) { console.error('上传失败:', error) // 处理错误和重试逻辑 } } // 所有分块上传完成后,通知服务器合并文件 await axios.post('/merge-chunks', { fileId: file.id, fileName: file.name, totalChunks }) }
5. 并发控制
限制同时上传的文件数量
const MAX_CONCURRENT_UPLOADS = 3 const uploadAllFiles = async () => { isUploading.value = true // 创建上传队列 const uploadQueue = [...files.value] const activeUploads = [] while (uploadQueue.length > 0 || activeUploads.length > 0) { // 填充活动上传队列 while (activeUploads.length < MAX_CONCURRENT_UPLOADS && uploadQueue.length > 0) { const file = uploadQueue.shift() const uploadPromise = uploadFile(file).finally(() => { // 上传完成后从活动队列中移除 activeUploads.splice(activeUploads.indexOf(uploadPromise), 1) }) activeUploads.push(uploadPromise) } // 等待至少一个上传完成 if (activeUploads.length > 0) { await Promise.race(activeUploads) } } isUploading.value = false }
6. 取消上传与暂停/恢复
实现上传控制功能
// 为每个文件创建取消令牌 const cancelTokens = {} const uploadFile = async (file) => { const cancelToken = axios.CancelToken.source() cancelTokens[file.id] = cancelToken try { await axios.post('/upload', formData, { cancelToken: cancelToken.token, onUploadProgress: (progressEvent) => { // 更新进度 } }) } catch (error) { if (axios.isCancel(error)) { console.log('上传已取消:', error.message) } else { console.error('上传错误:', error) } } } // 取消上传 const cancelUpload = (fileId) => { if (cancelTokens[fileId]) { cancelTokens[fileId].cancel('用户取消上传') } } // 暂停和恢复需要更复杂的实现,通常需要记录已上传的部分
7. 错误处理与重试机制
增强上传的可靠性
const uploadWithRetry = async (file, maxRetries = 3) => { let retries = 0 while (retries <= maxRetries) { try { await uploadFile(file) return // 上传成功,退出函数 } catch (error) { retries++ if (retries > maxRetries) { throw new Error(`上传失败: ${error.message}`) } // 等待一段时间后重试(指数退避) const delay = Math.pow(2, retries) * 1000 await new Promise(resolve => setTimeout(resolve, delay)) } } }
8. 文件预览与元数据展示
在上传前提供文件预览
const generateThumbnail = (file) => { return new Promise((resolve) => { if (!file.type.startsWith('image/')) { resolve(null) // 非图片文件不生成预览 return } const reader = new FileReader() reader.onload = (e) => { resolve(e.target.result) } reader.readAsDataURL(file) }) } // 处理文件选择时生成预览 const handleFileSelect = async (event) => { const selectedFiles = Array.from(event.target.files) for (const file of selectedFiles) { const thumbnail = await generateThumbnail(file) files.value.push({ id: generateUniqueId(), file, name: file.name, size: file.size, type: file.type, thumbnail, progress: 0, status: 'pending' }) } }
9. 响应式UI与用户体验优化
根据上传状态更新UI
<template> <div :class="['file-item', `status-${file.status}`]"> <div class="file-preview" v-if="file.thumbnail"> <img :src="file.thumbnail" :alt="file.name"> </div> <div class="file-info"> <div class="file-name">{{ file.name }}</div> <div class="file-size">{{ formatFileSize(file.size) }}</div> <div class="file-progress"> <div class="progress-bar"> <div class="progress-fill" :style="{ width: `${file.progress}%` }" ></div> </div> <div class="progress-text">{{ file.progress }}%</div> </div> </div> <div class="file-actions"> <button v-if="file.status === 'uploading'" @click="cancelUpload(file.id)" class="btn-cancel" > 取消 </button> <button v-if="file.status === 'error'" @click="retryUpload(file.id)" class="btn-retry" > 重试 </button> <button v-if="file.status !== 'uploading'" @click="removeFile(file.id)" class="btn-remove" > 删除 </button> </div> </div> </template>
完整示例
下面是一个简化的完整示例,展示了Vue3多文件上传的实现:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue3 多文件上传示例</title> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <style> .upload-container { max-width: 800px; margin: 0 auto; padding: 20px; } .drop-area { border: 2px dashed #ccc; border-radius: 8px; padding: 40px; text-align: center; margin-bottom: 20px; transition: all 0.3s; } .drop-area.dragover { border-color: #42b883; background-color: rgba(66, 184, 131, 0.1); } .file-list { margin-top: 20px; } .file-item { display: flex; align-items: center; padding: 12px; border: 1px solid #eee; border-radius: 6px; margin-bottom: 10px; } .file-preview { width: 50px; height: 50px; margin-right: 15px; background: #f5f5f5; display: flex; align-items: center; justify-content: center; border-radius: 4px; overflow: hidden; } .file-preview img { max-width: 100%; max-height: 100%; } .file-info { flex-grow: 1; } .file-name { font-weight: 500; margin-bottom: 5px; } .file-progress { margin-top: 8px; } .progress-bar { height: 6px; background: #e9ecef; border-radius: 3px; overflow: hidden; } .progress-fill { height: 100%; background: #42b883; transition: width 0.3s; } .file-actions { margin-left: 15px; } button { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; margin-left: 5px; } .btn-primary { background: #42b883; color: white; } .btn-cancel { background: #ff4757; color: white; } .btn-retry { background: #ffa502; color: white; } .btn-remove { background: #a4b0be; color: white; } .status-error { border-color: #ff4757; } .status-success { border-color: #42b883; } </style> </head> <body> <div id="app"> <div class="upload-container"> <h1>Vue3 多文件上传</h1> <div class="drop-area" :class="{ 'dragover': isDragging }" @drop="onDrop" @dragover.prevent="onDragOver" @dragleave="onDragLeave" > <p>拖放文件到此处或</p> <input type="file" multiple @change="onFileSelect" ref="fileInput" style="display: none;" > <button class="btn-primary" @click="openFileDialog">选择文件</button> </div> <div v-if="files.length > 0"> <div class="file-list"> <div v-for="file in files" :key="file.id" class="file-item" :class="'status-' + file.status" > <div class="file-preview"> <img v-if="file.thumbnail" :src="file.thumbnail" :alt="file.name"> <span v-else>📄</span> </div> <div class="file-info"> <div class="file-name">{{ file.name }}</div> <div class="file-size">{{ formatFileSize(file.size) }}</div> <div class="file-progress" v-if="file.status !== 'success'"> <div class="progress-bar"> <div class="progress-fill" :style="{ width: file.progress + '%' }" ></div> </div> <div>{{ file.progress }}%</div> </div> <div v-else>上传成功</div> </div> <div class="file-actions"> <button v-if="file.status === 'uploading'" @click="cancelUpload(file.id)" class="btn-cancel" > 取消 </button> <button v-if="file.status === 'error'" @click="retryUpload(file.id)" class="btn-retry" > 重试 </button> <button @click="removeFile(file.id)" class="btn-remove" > 删除 </button> </div> </div> </div> <div style="margin-top: 20px;"> <button class="btn-primary" @click="uploadFiles" :disabled="isUploading" > {{ isUploading ? '上传中...' : '开始上传' }} </button> <span style="margin-left: 15px;"> 总进度: {{ totalProgress }}% </span> </div> </div> </div> </div> <script> const { createApp, ref, reactive, computed } = Vue; createApp({ setup() { const files = ref([]); const isUploading = ref(false); const isDragging = ref(false); const fileInput = ref(null); // 模拟上传函数(实际项目中替换为真实上传逻辑) const simulateUpload = (fileId) => { return new Promise((resolve, reject) => { const file = files.value.find(f => f.id === fileId); if (!file) return; file.status = 'uploading'; const interval = setInterval(() => { if (file.progress < 95) { file.progress += Math.random() * 15; } else { clearInterval(interval); // 模拟成功或失败 setTimeout(() => { if (Math.random() > 0.2) { file.progress = 100; file.status = 'success'; resolve(); } else { file.status = 'error'; reject(new Error('上传失败')); } }, 500); } }, 300); }); }; const openFileDialog = () => { fileInput.value.click(); }; const onFileSelect = async (event) => { const selectedFiles = Array.from(event.target.files); await processFiles(selectedFiles); event.target.value = ''; // 重置input }; const onDrop = async (event) => { event.preventDefault(); isDragging.value = false; const droppedFiles = Array.from(event.dataTransfer.files); await processFiles(droppedFiles); }; const onDragOver = (event) => { event.preventDefault(); isDragging.value = true; }; const onDragLeave = (event) => { event.preventDefault(); isDragging.value = false; }; const processFiles = async (fileList) => { for (const file of fileList) { // 生成缩略图(如果是图片) const thumbnail = await generateThumbnail(file); files.value.push({ id: Date.now() + Math.random().toString(36).substr(2, 9), file, name: file.name, size: file.size, type: file.type, thumbnail, progress: 0, status: 'pending' }); } }; const generateThumbnail = (file) => { return new Promise((resolve) => { if (!file.type.startsWith('image/')) { resolve(null); return; } const reader = new FileReader(); reader.onload = (e) => { resolve(e.target.result); }; reader.readAsDataURL(file); }); }; const uploadFiles = async () => { isUploading.value = true; // 仅上传状态为pending的文件 const filesToUpload = files.value.filter(f => f.status === 'pending'); // 使用Promise.allSettled同时上传多个文件 const uploadPromises = filesToUpload.map(file => simulateUpload(file.id)); try { await Promise.allSettled(uploadPromises); console.log('所有文件上传完成'); } catch (error) { console.error('上传过程中出错:', error); } finally { isUploading.value = false; } }; const cancelUpload = (fileId) => { // 在实际项目中,这里应该取消正在进行的上传请求 const file = files.value.find(f => f.id === fileId); if (file) { file.status = 'cancelled'; } }; const retryUpload = (fileId) => { const file = files.value.find(f => f.id === fileId); if (file) { file.progress = 0; file.status = 'pending'; simulateUpload(fileId); } }; const removeFile = (fileId) => { files.value = files.value.filter(f => f.id !== fileId); }; const formatFileSize = (bytes) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const totalProgress = computed(() => { if (files.value.length === 0) return 0; const total = files.value.reduce((sum, file) => sum + file.progress, 0); return Math.round(total / files.value.length); }); return { files, isUploading, isDragging, fileInput, openFileDialog, onFileSelect, onDrop, onDragOver, onDragLeave, uploadFiles, cancelUpload, retryUpload, removeFile, formatFileSize, totalProgress }; } }).mount('#app'); </script> </body> </html>
关键技术点总结
- Composition API:使用Vue3的setup函数和ref/reactive管理状态
- 文件处理:通过File API处理用户选择的文件
- 拖放功能:利用HTML5拖放API实现拖放上传
- 预览生成:使用FileReader生成图片文件的缩略图预览
- 进度跟踪:模拟上传进度展示(实际项目中使用真实的上传进度事件)
- 并发控制:使用Promise.allSettled处理多个文件同时上传
- 响应式UI:根据文件状态动态更新界面
- 用户体验:提供取消、重试、删除等操作,增强交互性
这个实现涵盖了多文件上传的核心技术点,您可以根据实际需求进一步扩展和完善功能。
以上就是Vue3前端发送多个文件的方法详解的详细内容,更多关于Vue3发送多个文件的资料请关注脚本之家其它相关文章!