小程序附件上传的文件类型限制问题

问题背景

在小程序开发中,"附件上传"是常见功能。但很多开发者在实现时会遇到一个问题:用户只能选择图片,无法选择PDF、Word等其他类型的文件

最近在开发审批功能时就遇到了这个问题:审批申请需要上传附件(如合同、发票等),但用户反馈只能选择图片,无法选择其他文件。

问题现象

期望效果:
┌─────────────────────────────────────┐
│ 添加附件                             │
│ [图片] [PDF] [Word] [Excel] ...     │
└─────────────────────────────────────┘

实际效果:
┌─────────────────────────────────────┐
│ 添加附件                             │
│ [图片] [图片] [图片]  (只能选图片)   │
└─────────────────────────────────────┘

问题根因

小程序文件选择 API 对比

小程序提供了多个文件选择 API,各有不同的用途和限制:

API 用途 支持的文件类型 平台支持
uni.chooseImage() 选择图片 仅图片(jpg/png/gif等) 全平台
uni.chooseVideo() 选择视频 仅视频(mp4等) 全平台
uni.chooseFile() 选择文件 多种文件类型 H5/App(小程序不支持)
uni.chooseMessageFile() 从聊天记录选择 多种文件类型 仅微信小程序

问题代码

// 错误写法:只能选择图片
const chooseFile = () => {
  uni.chooseImage({
    count: 9,
    success: (res) => {
      // 只能获取到图片
      uploadFiles(res.tempFilePaths)
    }
  })
}

问题在于使用了 chooseImage,这个 API 顾名思义只能选择图片。

解决方案

方案一:使用 chooseMessageFile(微信小程序推荐)

微信小程序专属 API,允许用户从微信聊天记录中选择文件:

const chooseFile = () => {
  uni.chooseMessageFile({
    count: 9,
    type: 'file',  // 'all' | 'image' | 'video' | 'file'
    success: (res) => {
      // res.tempFiles 包含文件信息
      // [{ path, size, name, type }]
      const files = res.tempFiles.map(file => ({
        path: file.path,
        name: file.name,
        size: file.size,
        type: file.type
      }))
      uploadFiles(files)
    }
  })
}

优点

  • 支持选择各种文件类型(PDF、Word、Excel等)
  • 用户可以从聊天记录快速选择已有文件
  • 体验流畅,符合微信用户习惯

缺点

  • 仅微信小程序支持
  • 文件必须存在于聊天记录中

方案二:组合多个 API

提供多种选择入口,满足不同需求:

<template>
  <view class="upload-section">
    <view class="upload-title">添加附件</view>
    <view class="upload-buttons">
      <button @click="chooseImage">选择图片</button>
      <button @click="chooseFromChat">从聊天记录选择</button>
    </view>

    <!-- 已选文件列表 -->
    <view class="file-list">
      <view
        v-for="(file, index) in fileList"
        :key="index"
        class="file-item"
      >
        <image
          v-if="isImage(file)"
          :src="file.path"
          class="file-thumb"
        />
        <view v-else class="file-icon">
          {{ getFileIcon(file.name) }}
        </view>
        <text class="file-name">{{ file.name }}</text>
        <text class="file-delete" @click="removeFile(index)">删除</text>
      </view>
    </view>
  </view>
</template>

<script setup>
import { ref } from 'vue'

const fileList = ref([])

// 选择图片
const chooseImage = () => {
  uni.chooseImage({
    count: 9 - fileList.value.length,
    success: (res) => {
      const newFiles = res.tempFilePaths.map((path, index) => ({
        path,
        name: `图片${fileList.value.length + index + 1}.jpg`,
        type: 'image'
      }))
      fileList.value.push(...newFiles)
    }
  })
}

// 从聊天记录选择(微信小程序)
const chooseFromChat = () => {
  // #ifdef MP-WEIXIN
  uni.chooseMessageFile({
    count: 9 - fileList.value.length,
    type: 'file',
    success: (res) => {
      const newFiles = res.tempFiles.map(file => ({
        path: file.path,
        name: file.name,
        size: file.size,
        type: getFileType(file.name)
      }))
      fileList.value.push(...newFiles)
    }
  })
  // #endif

  // #ifndef MP-WEIXIN
  uni.showToast({
    title: '当前平台不支持',
    icon: 'none'
  })
  // #endif
}

// 判断是否为图片
const isImage = (file) => {
  return file.type === 'image' || /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)
}

// 获取文件图标
const getFileIcon = (fileName) => {
  const ext = fileName.split('.').pop().toLowerCase()
  const iconMap = {
    pdf: '📄',
    doc: '📝',
    docx: '📝',
    xls: '📊',
    xlsx: '📊',
    ppt: '📽️',
    pptx: '📽️',
    zip: '📦',
    rar: '📦'
  }
  return iconMap[ext] || '📎'
}

// 获取文件类型
const getFileType = (fileName) => {
  if (/\.(jpg|jpeg|png|gif|webp)$/i.test(fileName)) return 'image'
  if (/\.(mp4|mov|avi)$/i.test(fileName)) return 'video'
  return 'file'
}

// 删除文件
const removeFile = (index) => {
  fileList.value.splice(index, 1)
}
</script>

方案三:跨平台兼容封装

封装一个通用的文件选择函数,自动适配不同平台:

// utils/chooseFile.js

/**
 * 跨平台文件选择
 * @param {Object} options
 * @param {number} options.count - 最大选择数量
 * @param {string} options.type - 文件类型:'all' | 'image' | 'video' | 'file'
 * @returns {Promise<Array>} 文件列表
 */
export const chooseFile = (options = {}) => {
  const { count = 9, type = 'all' } = options

  return new Promise((resolve, reject) => {
    // 微信小程序:使用 chooseMessageFile
    // #ifdef MP-WEIXIN
    if (type === 'image') {
      uni.chooseImage({
        count,
        success: (res) => {
          resolve(res.tempFilePaths.map((path, i) => ({
            path,
            name: `image_${Date.now()}_${i}.jpg`,
            type: 'image'
          })))
        },
        fail: reject
      })
    } else {
      uni.chooseMessageFile({
        count,
        type: type === 'all' ? 'all' : type,
        success: (res) => {
          resolve(res.tempFiles.map(file => ({
            path: file.path,
            name: file.name,
            size: file.size,
            type: file.type
          })))
        },
        fail: reject
      })
    }
    // #endif

    // H5/App:使用 chooseFile
    // #ifdef H5 || APP-PLUS
    uni.chooseFile({
      count,
      type: type === 'all' ? 'all' : type,
      success: (res) => {
        resolve(res.tempFiles.map(file => ({
          path: file.path,
          name: file.name,
          size: file.size,
          type: file.type
        })))
      },
      fail: reject
    })
    // #endif
  })
}

使用方式:

import { chooseFile } from '@/utils/chooseFile'

// 选择任意文件
const files = await chooseFile({ count: 5, type: 'all' })

// 只选择图片
const images = await chooseFile({ count: 9, type: 'image' })

文件上传的完整流程

选择文件后,还需要上传到服务器:

/**
 * 上传文件到服务器
 * @param {Array} files - 文件列表
 * @returns {Promise<Array>} 上传结果
 */
export const uploadFiles = async (files) => {
  const uploadTasks = files.map(file => {
    return new Promise((resolve, reject) => {
      uni.uploadFile({
        url: 'https://api.example.com/upload',
        filePath: file.path,
        name: 'file',
        formData: {
          fileName: file.name,
          fileType: file.type
        },
        success: (res) => {
          const data = JSON.parse(res.data)
          resolve({
            ...file,
            url: data.url,  // 服务器返回的文件URL
            id: data.id
          })
        },
        fail: reject
      })
    })
  })

  return Promise.all(uploadTasks)
}

注意事项

1. 文件大小限制

小程序对上传文件大小有限制,建议在选择后进行校验:

const MAX_FILE_SIZE = 10 * 1024 * 1024  // 10MB

const validateFileSize = (files) => {
  const oversizedFiles = files.filter(f => f.size > MAX_FILE_SIZE)
  if (oversizedFiles.length > 0) {
    uni.showToast({
      title: `文件大小不能超过10MB`,
      icon: 'none'
    })
    return false
  }
  return true
}

2. 文件类型校验

防止用户上传不支持的文件类型:

const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'xls', 'xlsx']

const validateFileType = (files) => {
  const invalidFiles = files.filter(f => {
    const ext = f.name.split('.').pop().toLowerCase()
    return !ALLOWED_EXTENSIONS.includes(ext)
  })

  if (invalidFiles.length > 0) {
    uni.showToast({
      title: `不支持的文件类型`,
      icon: 'none'
    })
    return false
  }
  return true
}

3. 用户体验优化

  • 显示上传进度
  • 支持取消上传
  • 上传失败自动重试
const uploadWithProgress = (file, onProgress) => {
  return new Promise((resolve, reject) => {
    const task = uni.uploadFile({
      url: 'https://api.example.com/upload',
      filePath: file.path,
      name: 'file',
      success: (res) => resolve(JSON.parse(res.data)),
      fail: reject
    })

    // 监听上传进度
    task.onProgressUpdate((res) => {
      onProgress && onProgress(res.progress)
    })
  })
}

总结

  1. API 选择很重要chooseImage 只能选图片,需要选择其他文件类型时应使用 chooseMessageFile(微信)或 chooseFile(H5/App)

  2. 平台差异需处理:不同平台支持的 API 不同,建议封装统一的文件选择函数

  3. 完善的校验机制:文件大小、文件类型都需要校验,避免上传失败或服务器压力

  4. 良好的用户体验:提供多种选择方式、显示上传进度、处理异常情况


本文源于实际项目中的问题修复经验,希望对遇到类似问题的开发者有所帮助。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐