Taro3 + Vue3 小程序文件上传组件,支持 PDF/PPTX 跨端使用

本文介绍一个基于 Taro3 + Vue3 的文件上传组件,支持 PDF、PPTX 等文件上传,带进度提示、文件校验、双向绑定,适用于小程序/H5/React Native 多端。


📌 组件功能概述

功能 说明
📎 支持格式 PDF、PPTX
📦 文件大小限制 可配置(默认 20MB)
🔗 上传方式 调用封装的 OSS 上传接口
🔄 双向绑定 v-model + fileInfo
✨ 文件预览 PDF 下载,PPTX 显示文件名
🗑️ 删除功能 单个文件删除,自动更新绑定

🧩 组件完整代码

vue 复制代码
<template>
  <div class="file-wrap">
    <!-- 上传按钮 -->
    <div class="imgConetnt">
      <div class="img_Items" v-if="state.fileList.length < props.maxCount">
        <img
          :src="FileUpload"
          alt=""
          style="width: 64px; height: 64px"
          @click="openCam"
        />
      </div>
    </div>

    <!-- 文件列表 -->
    <view class="file-list">
      <view
        v-for="(item, index) in state.fileList"
        :key="index"
        class="item-file"
      >
        <view class="item-name" @click.stop="toDownload(item)">
          {{ item.name }}
        </view>
        <IconFont
          name="circle-close"
          class="close"
          @click.stop="onDelete(item, index)"
        ></IconFont>
      </view>
    </view>

    <!-- 提示信息 -->
    <view class="information" v-if="props.information">
      {{ props.information }}
    </view>
  </div>
</template>

<script setup lang="ts">
import { reactive, watch } from "vue";
import Taro from "@tarojs/taro";

// ========== API 引入(请替换为你的实际上传接口) ==========
// import { apixx } from "@/api/xx";
// import { useLoginStore } from "@/stores/login";
// import { IconFont } from "@nutui/icons-vue-taro";

// ========== Props 定义 ==========
const props = defineProps({
  // 双向绑定:文件ID列表,逗号分隔
  modelValue: { type: String, default: "" },
  // 业务ID
  id: { type: String, default: "" },
  // 最大文件数量
  maxCount: { type: Number, default: 5 },
  // 单个文件最大大小(MB)
  maxSize: { type: Number, default: 20 },
  // 是否必填
  required: { type: Boolean, default: false },
  // 提示文字
  information: { type: String, default: "" },
  // 上传类型:image/file
  uploadType: { type: String, default: "file" },
  // 上传接口地址(必填)
  action: { type: String, required: true, default: "" },
});

// ========== Emit 定义 ==========
const emit = defineEmits(["update:modelValue", "update:fileInfo", "change"]);

// ========== 状态 ==========
const state: any = reactive({
  maxCount: props.maxCount ? Number(props.maxCount) : 1,
  fileList: [],
});

// ========== 回显(编辑时显示已有文件) ==========
watch(
  () => props.modelValue,
  (value) => {
    if (props.id && value) {
      const result: any = [];
      value.split(",").forEach((item) => result.push(item));
      state.fileList = result;
    }
    if (value == null) {
      state.fileList = [];
      state.maxCount = 5;
    }
  },
  { immediate: true }
);

// ========== 文件预览/下载 ==========
const toDownload = async (item) => {
  // 请替换为你的下载方法
  // await LoginStore.downloadFile(item);
};

// ========== 删除文件 ==========
const onDelete = (item: any, index: number) => {
  const fileIds = state.fileList.map((item: any) => item.fileId);
  state.fileList.splice(index, 1);
  emit("update:fileInfo", [...state.fileList]);
  emit("update:modelValue", [...fileIds].join(","));
  state.maxCount = props.maxCount - state.fileList.length;
};

// ========== 选择文件 ==========
const openCam = () => {
  Taro.chooseMessageFile({
    type: "file",
    count: 1,
    extension: ["pdf", "pptx"],
    success: async (res) => {
      const tempFiles = res.tempFiles;
      if (!tempFiles?.length) {
        return Taro.showToast({ title: "未选择文件", icon: "none" });
      }

      const file = tempFiles[0];
      const fileName = file.name || "";

      // 文件类型校验
      if (
        !fileName.endsWith(".pdf") &&
        !fileName.endsWith(".PDF") &&
        !fileName.endsWith(".pptx")
      ) {
        return Taro.showToast({
          title: "请选择 PDF 或 PPTX 文件",
          icon: "none",
        });
      }

      // 文件大小校验
      if (file.size > props.maxSize * 1024 * 1024) {
        return Taro.showToast({
          title: `文件过大,建议不超过 ${props.maxSize}MB`,
          icon: "none",
        });
      }

      Taro.showLoading({ title: "上传中..." });
      const list = await uploadPdf(file.path);
      state.fileList = [...state.fileList, ...list];

      const fileIds = state.fileList
        .map((item: any) => item.fileId)
        .join(",");
      state.maxCount = props.maxCount - state.fileList.length;

      emit("update:fileInfo", state.fileList);
      emit("update:modelValue", fileIds);
      Taro.hideLoading();
    },
    fail: (err) => {
      console.error("选择文件失败:", err);
      Taro.hideLoading();
    },
  });
};

// ========== 上传文件核心方法 ==========
/**
 * 上传文件到 OSS
 * @param filePath - Taro 临时文件路径
 * @returns 上传成功返回 { fileId, url, name }
 */
const uploadPdf = async (filePath: string) => {
  try {
    // ⭐ 请替换为你的实际上传接口
    // const res: any = await uploadFileOss({ filePath });
    const res: any = await Taro.uploadFile({
      url: "https://your-upload-api.com/upload", // ⚠️ 替换为实际上传地址
      filePath,
      name: "file",
      formData: { type: "file" },
    });

    const data = JSON.parse(res.data);
    if (data.code === 200) {
      return [
        {
          fileId: data.data.fileId || data.data.id,
          url: data.data.fileUrl || data.data.url,
          name: decodeURIComponent(data.data.fileName || data.data.name),
        },
      ];
    } else {
      Taro.showToast({ title: "上传失败", icon: "none" });
    }
  } catch (error) {
    Taro.hideLoading();
    throw error;
  }
};
</script>

<style lang="scss" scoped>
.file-wrap {
  .information {
    font-size: 14px;
    color: rgba(0, 0, 0, 0.5);
    margin-top: 8px;
  }

  .imgConetnt {
    display: flex;
    flex-wrap: wrap;
  }

  .file-list {
    display: flex;
    align-items: center;
    flex-direction: column;

    .item-file {
      display: flex;
      align-items: center;
      justify-content: center;
      margin-top: 8px;

      .item-name {
        color: #007aff;
        word-wrap: break-word;
        word-break: break-all;
        text-decoration: underline;
        display: inline;
      }

      .close {
        margin-left: 8px;
      }
    }
  }

  .img_Items {
    border-radius: 4px;
    margin-right: 9px;
    margin-bottom: 10px;
  }
}
</style>

📖 使用方法

vue 复制代码
<template>
  <FileUpload
    v-model="fileIds"
    :max-count="3"
    :max-size="20"
    information="支持 PDF、PPTX,不超过 20MB"
    @update:file-info="handleFileInfo"
  />
</template>

<script setup>
import FileUpload from "@/components/FileUpload.vue";

const fileIds = ref(""); // 绑定文件ID列表

const handleFileInfo = (list) => {
  console.log("当前文件列表:", list);
};
</script>

🔑 核心逻辑解析

  1. 双绑定的实现
typescript 复制代码
// 父组件 → 子组件:通过 props.modelValue 回显文件
watch(() => props.modelValue, (value) => {
  if (value) state.fileList = value.split(',');
}, { immediate: true });

// 子组件 → 父组件:通过 emit 更新
emit("update:modelValue", fileIds);  // 触发 v-model 更新
emit("update:fileInfo", state.fileList); // 附加文件详情列表
  1. Taro 文件上传流程

    Taro.chooseMessageFile() → 选择文件

    文件大小 & 类型校验

    Taro.uploadFile() → 上传到 OSS

    解析返回的 fileId / url

    更新 fileList + 触发双绑定

  2. 多端兼容性
    端 chooseMessageFile uploadFile
    微信小程序 ✅ ✅
    H5 ✅ ✅
    React Native ❌ ✅(需换方案)
    支付宝小程序 ✅ ✅

⚠️ 注意: Taro.chooseMessageFile 仅支持小程序和H5端,RN端需使用其他方案(如 react-native-document-picker)。


⚠️ 踩坑记录

坑1:文件类型校验不生效

问题: chooseMessageFile 在某些端不支持 extension 参数过滤。

解决: 在 success 回调中手动校验文件扩展名,上传前再判断一次。

typescript 复制代码
const fileName = file.name || '';
if (!fileName.match(/\.(pdf|pptx)$/i)) {
  return Taro.showToast({ title: '格式不支持', icon: 'none' });
}

坑2:uploadFile 返回格式

问题: Taro.uploadFile 返回的是字符串,需要手动 JSON.parse

解决:

typescript 复制代码
const data = JSON.parse(res.data);

坑3:H5 端跨域

问题: 上传接口若在不同域名,会遇到 CORS 跨域。
解决: 让后端配置 Access-Control-Allow-Origin,或通过Nginx反向代理。

🛠️ 完整文件上传接口示例(后端参考)

javascript 复制代码
// Node.js / Express 示例(仅供参考)
const multer = require('multer');
const path = require('path');
const COS = require('cos-nodejs-sdk-v5');

const cos = new COS({
  SecretId: process.env.COS_SECRET_ID,      // ⚠️ 替换为环境变量
  SecretKey: process.env.COS_SECRET_KEY,      // ⚠️ 替换为环境变量
});

const upload = multer({ dest: '/tmp/uploads' });

app.post('/upload', upload.single('file'), async (req, res) => {
  const file = req.file;
  try {
    const result = await cos.putObject({
      Bucket: 'your-bucket',
      Region: 'ap-shanghai',
      Key: `uploads/${Date.now()}-${file.originalname}`,
      FilePath: file.path,
    });
    res.json({
      code: 200,
      data: {
        fileId: result.ETag,
        fileUrl: `https://your-cdn.com/${result.Key}`,
        fileName: file.originalname,
      }
    });
  } catch (err) {
    res.status(500).json({ code: 500, message: '上传失败' });
  }
});

✅ 总结

这个组件麻雀虽小,五脏俱全:
Vue3 Composition API --- 写法现代,逻辑清晰
Taro 跨端 --- 一套代码支持小程序/H5
TypeScript --- 类型安全,可维护性强
双向绑定 --- 符合 Vue 规范,上手即用
如果对你有帮助,欢迎收藏 + 点赞!

相关推荐
OctShop大型商城源码2 小时前
商城小程序开源源码_大型免费开源小程序商城_OctShop
小程序·开源·商城小程序开源源码·免费开源小程序商城
吹个口哨写代码2 小时前
h5/小程序直接读本地/在线的json文件数据
前端·小程序·json
2501_9159214319 小时前
苹果iOS应用开发上架与推广完整教程
android·ios·小程序·https·uni-app·iphone·webview
2501_9151063220 小时前
HTTP和HTTPS协议工作原理及安全性全面解析
android·ios·小程序·https·uni-app·iphone·webview
禾高网络21 小时前
长护险智慧服务平台:科技赋能长期照护保障体系
大数据·人工智能·科技·小程序
杰建云1671 天前
小程序转化率低的核心原因是什么?
小程序·小程序制作
中国胖子风清扬1 天前
基于GPUI框架构建现代化待办事项应用:从架构设计到业务落地
java·spring boot·macos·小程序·rust·uni-app·web app
weikecms1 天前
外卖CPS小程序哪家系统比较完善
小程序
绝世唐门三哥2 天前
uniapp系列-uniappp都有哪些生命周期?
vue.js·小程序·uniapp