vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue3发送多个文件

Vue3前端发送多个文件的方法详解

作者:BillKu

这篇文章主要介绍了基于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函数替换为真实的上传逻辑,通常使用fetchaxios库来实现文件上传。真实上传代码可能类似于:

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>

关键技术点总结

  1. Composition API:使用Vue3的setup函数和ref/reactive管理状态
  2. 文件处理:通过File API处理用户选择的文件
  3. 拖放功能:利用HTML5拖放API实现拖放上传
  4. 预览生成:使用FileReader生成图片文件的缩略图预览
  5. 进度跟踪:模拟上传进度展示(实际项目中使用真实的上传进度事件)
  6. 并发控制:使用Promise.allSettled处理多个文件同时上传
  7. 响应式UI:根据文件状态动态更新界面
  8. 用户体验:提供取消、重试、删除等操作,增强交互性

这个实现涵盖了多文件上传的核心技术点,您可以根据实际需求进一步扩展和完善功能。

以上就是Vue3前端发送多个文件的方法详解的详细内容,更多关于Vue3发送多个文件的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文