本文介绍一个基于 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>
🔑 核心逻辑解析
- 双绑定的实现
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); // 附加文件详情列表
-
Taro 文件上传流程
Taro.chooseMessageFile() → 选择文件
↓
文件大小 & 类型校验
↓
Taro.uploadFile() → 上传到 OSS
↓
解析返回的 fileId / url
↓
更新 fileList + 触发双绑定 -
多端兼容性
端 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 规范,上手即用
如果对你有帮助,欢迎收藏 + 点赞!