打造高效实用的文件上传组件:基于 Uni-app 的完整实现方案

在移动应用和跨平台开发中,文件上传是一个常见且关键的功能模块。一个设计良好的文件上传组件不仅能提升用户体验,还能减少开发者的重复劳动。今天我将分享一个基于 Uni-app 框架开发的通用文件上传组件,它具备文件选择、验证、上传进度显示、批量操作等完整功能,适用于多种业务场景。

组件设计思路

在开发文件上传组件前,我们需要明确核心需求:

  • 支持多种文件类型的选择与过滤
  • 提供文件大小限制功能
  • 显示上传进度和状态
  • 支持单个和批量上传操作
  • 允许移除已选文件
  • 提供清晰的视觉反馈和错误提示
  • 具备良好的可扩展性和复用性

基于这些需求,我设计了一个名为FileUploader的组件,采用模块化结构,将 UI 展示与业务逻辑分离,同时通过 props 和 events 实现与父组件的灵活交互。

组件完整实现

下面是组件的完整代码实现,我会分模块进行解析:

模板结构(Template)

模板部分主要包含三个核心区域:上传按钮、已选文件列表和批量上传按钮,采用了清晰的层次结构和语义化命名。

<template>
  <view class="file-uploader">
    <!-- 上传按钮 -->
    <view class="upload-btn" @click="chooseFile">
      <view class="icon">
        <uni-icons type="plus" size="24" color="#666"></uni-icons>
      </view>
      <text class="text">点击上传文件</text>
      <text class="hint">支持 docx、doc、pdf 格式,最大 10MB</text>
    </view>

    <!-- 已选文件列表 -->
    <view class="file-list" v-if="fileList.length > 0">
      <view class="file-item" v-for="(file, index) in fileList" :key="index">
        <!-- 文件图标 -->
        <view class="file-icon">
          <uni-icons 
            :type="getFileIcon(file.name)" 
            size="28" 
            color="#409eff"
          ></uni-icons>
        </view>
        
        <!-- 文件信息 -->
        <view class="file-info">
          <text class="file-name">{{ file.name }}</text>
          <text class="file-size">{{ formatFileSize(file.size) }}</text>
          
          <!-- 上传进度 -->
          <view class="progress-bar" v-if="file.uploading">
            <view 
              class="progress" 
              :style="{ width: file.progress + '%' }"
            ></view>
          </view>
          
          <!-- 上传状态 -->
          <text class="status" v-if="!file.uploading">
            {{ file.success ? '上传成功' : '等待上传' }}
          </text>
        </view>
        
        <!-- 操作按钮 -->
        <view class="file-actions">
          <uni-icons 
            type="trash" 
            size="20" 
            color="#f56c6c"
            @click.stop="removeFile(index)"
          ></uni-icons>
          
          <uni-icons 
            type="cloud-upload" 
            size="20" 
            color="#409eff"
            @click.stop="uploadSingleFile(index)"
            v-if="!file.uploading && !file.success"
          ></uni-icons>
        </view>
      </view>
    </view>
    
    <!-- 批量上传按钮 -->
    <button 
      class="batch-upload-btn"
      type="primary"
      @click="uploadAllFiles"
      v-if="fileList.length > 0 && hasFilesToUpload"
    >
      批量上传
    </button>
  </view>
</template>

js逻辑(JavaScript)

实现了文件选择、验证、上传、进度监听等核心功能,通过清晰的函数划分提高了代码的可读性和可维护性。

<script>
export default {
  name: 'FileUploader',
  props: {
    // 上传接口地址
    uploadUrl: {
      type: String,
      required: true
    },
    // 额外的上传参数
    uploadParams: {
      type: Object,
      default: () => ({})
    },
    // 最大文件大小(MB)
    maxSize: {
      type: Number,
      default: 10
    },
    // 支持的文件类型
    fileTypes: {
      type: Array,
      default: () => ['docx', 'doc', 'pdf']
    }
  },
  data() {
    return {
      fileList: [] // 文件列表
    }
  },
  computed: {
    // 是否有等待上传的文件
    hasFilesToUpload() {
      return this.fileList.some(file => !file.uploading && !file.success)
    }
  },
  methods: {
    // 选择文件
    chooseFile() {
      const that = this
      // 转换支持的文件类型为微信小程序所需的格式
      const acceptTypes = this.fileTypes.map(type => `.${type}`)
      
      uni.chooseMessageFile({
        count: 5, // 最多选择5个文件
        type: 'file',
        extension: acceptTypes,
        success(res) {
          // 处理选择的文件
          res.tempFiles.forEach(file => {
            that.validateAndAddFile(file)
          })
        }
      })
    },
    
    // 验证文件并添加到列表
    validateAndAddFile(file) {
      // 获取文件扩展名
      const ext = file.name.split('.').pop().toLowerCase()
      
      // 验证文件类型
      if (!this.fileTypes.includes(ext)) {
        uni.showToast({
          title: `不支持${ext}格式,请上传${this.fileTypes.join('、')}文件`,
          icon: 'none',
          duration: 3000
        })
        return false
      }
      
      // 验证文件大小 (转换为MB)
      const fileSizeMB = file.size / 1024 / 1024
      if (fileSizeMB > this.maxSize) {
        uni.showToast({
          title: `文件大小不能超过${this.maxSize}MB`,
          icon: 'none',
          duration: 3000
        })
        return false
      }
      
      // 添加到文件列表
      this.fileList.push({
        name: file.name,
        path: file.path,
        size: file.size,
        progress: 0,
        uploading: false,
        success: false,
        result: null
      })
      
      return true
    },
    
    // 移除文件
    removeFile(index) {
      this.fileList.splice(index, 1)
    },
    
    // 上传单个文件
    uploadSingleFile(index) {
      const file = this.fileList[index]
      if (!file || file.uploading || file.success) return
      
      // 设置上传状态
      this.$set(this.fileList[index], 'uploading', true)
      this.$set(this.fileList[index], 'progress', 0)
      
      const that = this
      const uploadTask = uni.uploadFile({
        url: this.uploadUrl,
        filePath: file.path,
        name: 'file', // 后端接收文件的参数名
        formData: this.uploadParams,
        success(res) {
          // 处理上传成功
          that.$set(that.fileList[index], 'uploading', false)
          that.$set(that.fileList[index], 'success', true)
          that.$set(that.fileList[index], 'result', JSON.parse(res.data))
          
          // 通知父组件
          that.$emit('uploadSuccess', {
            index,
            file: that.fileList[index],
            response: JSON.parse(res.data)
          })
        },
        fail(err) {
          // 处理上传失败
          that.$set(that.fileList[index], 'uploading', false)
          that.$set(that.fileList[index], 'success', false)
          
          uni.showToast({
            title: '上传失败,请重试',
            icon: 'none',
            duration: 2000
          })
          
          // 通知父组件
          that.$emit('uploadFail', {
            index,
            file: that.fileList[index],
            error: err
          })
        }
      })
      
      // 监听上传进度
      uploadTask.onProgressUpdate(progress => {
        this.$set(this.fileList[index], 'progress', progress.progress)
        this.$emit('uploadProgress', {
          index,
          file: this.fileList[index],
          progress: progress.progress
        })
      })
    },
    
    // 批量上传所有未上传的文件
    uploadAllFiles() {
      this.fileList.forEach((file, index) => {
        if (!file.uploading && !file.success) {
          this.uploadSingleFile(index)
        }
      })
    },
    
    // 根据文件名获取对应的图标
    getFileIcon(fileName) {
      const ext = fileName.split('.').pop().toLowerCase()
      switch (ext) {
        case 'doc':
        case 'docx':
          return 'file-text'
        case 'pdf':
          return 'document'
        default:
          return 'file'
      }
    },
    
    // 格式化文件大小显示
    formatFileSize(bytes) {
      if (bytes < 1024) return bytes + ' B'
      else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'
      else return (bytes / 1048576).toFixed(2) + ' MB'
    }
  }
}
</script>

样式设计(Style)

样式部分采用了清晰的层次结构和响应式设计,确保在不同设备上都能提供良好的视觉体验。

<style scoped>
.file-uploader {
  width: 100%;
  padding: 16rpx;
  box-sizing: border-box;
}

/* 上传按钮样式 */
.upload-btn {
  width: 100%;
  height: 200rpx;
  border: 2rpx dashed #ccc;
  border-radius: 12rpx;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #f9f9f9;
  margin-bottom: 30rpx;
}

.upload-btn .icon {
  margin-bottom: 16rpx;
}

.upload-btn .text {
  font-size: 30rpx;
  color: #666;
  margin-bottom: 8rpx;
}

.upload-btn .hint {
  font-size: 24rpx;
  color: #999;
}

/* 文件列表样式 */
.file-list {
  margin-bottom: 30rpx;
}

.file-item {
  display: flex;
  align-items: center;
  padding: 20rpx 0;
  border-bottom: 1rpx solid #eee;
}

.file-icon {
  margin-right: 20rpx;
  width: 60rpx;
  text-align: center;
}

.file-info {
  flex: 1;
  min-width: 0;
}

.file-name {
  font-size: 28rpx;
  color: #333;
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 8rpx;
}

.file-size {
  font-size: 22rpx;
  color: #999;
  display: block;
  margin-bottom: 8rpx;
}

/* 进度条样式 */
.progress-bar {
  width: 100%;
  height: 8rpx;
  background-color: #eee;
  border-radius: 4rpx;
  overflow: hidden;
}

.progress {
  height: 100%;
  background-color: #409eff;
  transition: width 0.3s ease;
}

/* 状态文本 */
.status {
  font-size: 24rpx;
  margin-top: 8rpx;
  display: inline-block;
}

.status.success {
  color: #67c23a;
}

/* 文件操作按钮 */
.file-actions {
  display: flex;
  align-items: center;
  gap: 20rpx;
  margin-left: 20rpx;
}

/* 批量上传按钮 */
.batch-upload-btn {
  width: 100%;
  height: 80rpx;
  line-height: 80rpx;
  font-size: 30rpx;
  border-radius: 12rpx;
}
</style>

核心功能解析

这个文件上传组件包含多个核心功能模块,每个模块都经过精心设计以确保用户体验和功能完整性:

1. 文件选择与验证

  • 文件选择:使用uni.chooseMessageFileAPI 实现跨平台的文件选择功能,支持一次选择多个文件
  • 类型验证:通过解析文件扩展名,验证是否为允许的文件类型,并提供清晰的错误提示
  • 大小验证:将文件大小转换为 MB 单位,与允许的最大尺寸比较,防止超大文件上传

2. 文件列表管理

  • 采用数组存储已选文件的信息,包括名称、路径、大小、上传状态等
  • 实现了文件移除功能,允许用户随时删除不需要的文件
  • 通过计算属性hasFilesToUpload智能判断是否显示批量上传按钮

3. 上传功能实现

  • 单个上传:通过uni.uploadFileAPI 实现文件上传,支持自定义上传地址和额外参数
  • 批量上传:遍历文件列表,对未上传的文件依次执行上传操作
  • 进度监听:利用uploadTask.onProgressUpdate实时获取上传进度,并更新 UI 展示

4. 状态管理与反馈

  • 为每个文件维护完整的状态信息:上传中、上传成功、等待上传
  • 通过进度条直观展示上传进度
  • 使用 Toast 提示上传结果和错误信息
  • 根据文件类型显示不同的图标,增强视觉识别性

5. 事件交互

组件通过以下事件与父组件进行交互,提供足够的灵活性:

  • uploadSuccess:文件上传成功时触发
  • uploadFail:文件上传失败时触发
  • uploadProgress:上传进度更新时触发

使用示例

使用这个组件非常简单,只需在父组件中引入并配置必要的参数:

<template>
  <view>
    <FileUploader 
      uploadUrl="https://your-api.com/upload"
      :uploadParams="{ category: 'document' }"
      :maxSize="20"
      :fileTypes="['docx', 'doc', 'pdf', 'xlsx']"
      @uploadSuccess="handleUploadSuccess"
      @uploadFail="handleUploadFail"
      @uploadProgress="handleUploadProgress"
    />
  </view>
</template>

<script>
import FileUploader from '@/components/FileUploader.vue'

export default {
  components: {
    FileUploader
  },
  methods: {
    handleUploadSuccess(data) {
      console.log('上传成功', data)
      // 处理上传成功后的逻辑,如保存文件ID等
    },
    handleUploadFail(data) {
      console.error('上传失败', data)
      // 处理上传失败后的逻辑
    },
    handleUploadProgress(data) {
      console.log('上传进度', data)
      // 可用于全局进度展示
    }
  }
}
</script>

组件亮点

这个文件上传组件具有以下亮点:

  1. 跨平台兼容:基于 Uni-app 开发,可同时运行在微信小程序、H5、App 等多个平台
  2. 高度可配置:通过 props 灵活配置上传地址、文件类型、大小限制等参数
  3. 用户体验佳:提供清晰的视觉反馈和操作指引,减少用户认知成本
  4. 代码规范:结构清晰,命名规范,注释完善,易于维护和扩展
  5. 功能完整:涵盖了文件上传所需的全部核心功能,开箱即用

总结

本文介绍的FileUploader组件是一个功能完整、设计精良的文件上传解决方案。它不仅实现了文件选择、验证、上传、进度展示等核心功能,还通过精心的 UI 设计和交互细节提升了用户体验。

组件的主要优势在于:

  1. 完整性:一站式解决文件上传的所有需求,无需额外开发
  2. 灵活性:通过参数配置和事件交互,可适应不同的业务场景
  3. 易用性:简单的引入方式和清晰的 API 设计,降低使用门槛
  4. 可扩展性:模块化的代码结构使得功能扩展和定制变得容易

在实际项目中,你可以根据具体需求进一步扩展这个组件,例如添加文件预览功能、支持断点续传、增加拖拽上传等特性。希望这个组件能帮助你快速实现高质量的文件上传功能,提升开发效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值