Vue3+TS 实现批量拖拽文件夹上传图片组件封装
1、html 代码:
代码中的表格引入了 vxe-table 插件
<Tag /> 是自己封装的说明组件
表格列表这块我使用了插槽来增加扩展性,可根据自己需求,在组件外部做调整
<template><div class="dragUpload"><el-dialog v-model="data.visible"width="35%"center:draggable="draggable":destroy-on-close="true":close-on-click-modal="false":close-on-press-escape="false":before-close="closeDialogFn"><template #header><h3>{{ fileData.step === 2 ? '提示:文件超出大小限制' : '拖拽上传'}}</h3></template><!-- 文件上传区域 --><div class="drag-box"v-if="fileData.step === 1"@dragover="handleDragOver"@dragleave="handleDragOver"@drop="handleDrop"><div class="div-text" ><div class="drag-tip">拖拽文件至此区域<span class="click-txt" @click="toUploadFloder">点击上传</span></div><div class="btn-wrap"><el-button v-if="singleFile" @click="toUploadFile">上传文件</el-button><el-button @click="toUploadFloder">上传文件夹</el-button><inputv-if="singleFile":style="{ display: 'none' }"type="file"ref="fileUploadRef"@change="handleFileChange"multiple/><input:style="{ display: 'none' }"type="file"ref="fileUploadFloderRef"@change="handleFloderChange"webkitdirectorymultiple/></div></div></div><!-- 超出限制后,展示的列表 --><div v-if="fileData.step === 2"><!-- 组件内默认展示 --><div class="max-h311" v-if="!openSlot"><vxe-table:data="fileData.goBeyondTable"height="100%":checkbox-config="{showHeader: true,trigger: 'cell'}"><template #empty><div class="no-data-box"><i class="icon_noData"></i><div class="m-t6">暂无数据</div></div></template><vxe-column min-width="180" :title="`${data.fileName || 'SPU'}文件名`"><template #default="{ row }">{{ row[fileData.firstFileName] ? row[fileData.firstFileName] : '--' }}</template></vxe-column><vxe-column min-width="111" title="大小"><template #default="{ row }">{{ row.size ? row.size : '--' }} MB</template></vxe-column><vxe-column width="100" title="操作" :visible="fileData.goBeyondTable.length > 1"><template #default="{ $rowIndex, row }"><el-tooltipcontent="移除"placement="top":hide-after="0"><a class="icon_delete f-s18"href="javscript:"@click="handleDelete($rowIndex, row)"></a></el-tooltip></template></vxe-column></vxe-table></div><!-- 插槽,可使用外部传入 --><template v-else><slot name="errorTable"></slot></template></div><Tag class="m-t12"v-if="data.fileSize":content="fileData.step === 1 ? `文件大小不能超过 ${data.fileSize} MB` : `文件大小不能超过 ${data.fileSize}MB,目前文件大小 ${fileData.allSize} MB`"/><template #footer v-if="fileData.step === 2"><div class="dialog-footer"><el-button @click="closeDialogFn">取消</el-button><!-- 默认使用内部提交逻辑 --><template v-if="!openSlot"><el-button type="primary" @click="handleUploadToServer(true)">提交</el-button></template><!-- 开启插槽,则使用外部自定义 --><template v-else><slot name="footerBtn"></slot></template></div></template></el-dialog></div>
</template>
2、js 代码:
这块主要思路是:将文件夹判断后进行递归,获取出文件夹中的文件出来,最后类似单个文件上传,然后将文件流进行遍历 append 进创建的 FormData 对象。具体方法看:readFiles()
<script lang="ts" setup>
import { reactive, ref, getCurrentInstance } from 'vue';const { proxy }: any = getCurrentInstance();
const $tool = proxy.$tool;const props = defineProps({// 组件参数配置data: {type: Object,default: () => ({// fileSize: 100, // 文件大小限制// imgType: ['png', 'jpg', 'jpeg'], // 图片类型限制// fileName: '', // 超出后列表展示的文件名:不传默认为SPU}),},// 是否支持窗口拖拽,默认truedraggable: {type: Boolean,default: true},// 是否支持打开 file 单文件上传,不传默认falsesingleFile: {type: Boolean,default: false},//是否需要开启:列表上传失败 和 提交按钮插槽:默认不开启,展示组件内的失败列表 和 提交逻辑openSlot: {type: Boolean,default: false}
});/*** @param dragUploadAxiosFn 上传接口抛出,外部做处理,不与组件内部逻辑耦合* @param dragUploadErrorTable 超出限制后,展示的列表插槽抛出,外部做处理,不与组件内部逻辑耦合*/
const emit = defineEmits(['dragUploadAxiosFn', 'dragUploadErrorTable']);const fileUploadRef = ref();
const fileUploadFloderRef = ref();const fileData: any = reactive({step: 1, // 步骤: 1:文件拖拽上传;2:文件超出提示uploadList: [], //上传的文件列表waitUploadList: [], //存储待上传的文件列表fileSizeList: [], //存储遍历出来文件里面所有的图片路径及大小goBeyondTable: [], //超出限制后,将遍历项还原成文件夹项展示的列表allSize: 0, //文件总大小 MBfirstFileName: 'pathName0' //第一列字段:key
});/*文件上传input*/
const toUploadFile = () => {fileUploadRef.value.click();
};/*文件夹上传input*/
const toUploadFloder = () => {fileUploadFloderRef.value.click();
};/*选择文件改变*/
const handleFileChange = (e: any) => {if (e.target.files) {let filesList: any = Array.from(e.target.files);filesList.forEach((item: any) => {let size = item.size / 1024 / 1024;fileData.allSize += size;let obj: any = getPath(item.name);changeFileSizeList(item, obj);});fileData.allSize = fileData.allSize.toFixed(2); // 文件总大小 MBfileData.waitUploadList = filesList;if (!fileLimitFn(fileData.fileSizeList)) return; // 校验方法handleUploadToServer(); //上传文件到服务器}
};/*文件夹目录上传*/
const handleFloderChange = (e: any) => {if (e.target.files) {let filesList: any = Array.from(e.target.files);filesList.forEach((item: any) => { let size = item.size / 1024 / 1024;fileData.allSize += size;let obj: any = getPath(item.webkitRelativePath); // 通过路径获取名称方法changeFileSizeList(item, obj);});filesList.reverse(); // 反转数组,保证最先选择的文件排在最后面fileData.allSize = fileData.allSize.toFixed(2); // 文件总大小 MBfileData.waitUploadList = filesList;if (!fileLimitFn(fileData.fileSizeList)) return; // 校验方法handleUploadToServer(); //上传文件到服务器}
};// 拖放进入目标区域
const handleDragOver = (event) => {event.preventDefault();
};// 拖拽放置
const handleDrop = async (event) => {event.preventDefault();const files = [];const promises: any[] = [];for (const item of event.dataTransfer.items) {const entry: any = item.webkitGetAsEntry();if (!entry.isDirectory && !props.singleFile) {proxy.$message.error(`只支持文件夹上传,不支持单个文件上传!`);return;}promises.push(readFiles(entry));}const resultFilesArrays = await Promise.all(promises); // 等待所有文件读取完成fileData.waitUploadList = resultFilesArrays.flat();if (!fileLimitFn(fileData.fileSizeList)) return; // 校验方法handleUploadToServer(); //上传文件到服务器
};//文件各种限制判断方法封装
const fileLimitFn = (fileSizeList: any) => {console.log('fileData.fileSizeList', fileData.fileSizeList);//文件大小超出限制if (props.data.fileSize) {if (!fileSizeLimit(fileSizeList)) { fileData.goBeyondTable = getGoBeyondTable(fileSizeList);fileData.step = 2;return false;}}//图片类型限制判断if (props.data.imgType) {if (!fileImgType(fileSizeList)) return false;}return true;
};//文件超出限制
const fileSizeLimit = (fileSizeList: any) => {let allSize = fileSizeList.reduce((accumulator, currentValue) => {if (currentValue) {return accumulator + currentValue.size;}return accumulator;}, 0);let fileSize = props.data.fileSize * 1024 * 1024;fileData.allSize = (allSize / 1024 / 1024).toFixed(2); //存储文件总大小 MBif (allSize > fileSize) {proxy.$message.error(`文件大小不能超过 ${props.data.fileSize} MB`);emit('dragUploadErrorTable', fileData);return false;}return true;
};// 文件中图片,格式类型判断
const fileImgType = (fileSizeList: any) => {let findList: any = fileSizeList.filter((item: any) => !props.data.imgType.includes(item.type));if (findList.length > 0) {proxy.$message.error(`图片只能上传 ${props.data.imgType} 格式`);return false;};return true;
};// 操作数据:文件超出后,展示的列表
const getGoBeyondTable = (fileSizeList: any) => {// 遍历,相同第一列为一项,size累加let result: any = fileSizeList.reduce((accumulator, current) => {if (accumulator[current[fileData.firstFileName]]) { //如果已经存在,则累加sizeaccumulator[current[fileData.firstFileName]].size += current.size;} else {accumulator[current[fileData.firstFileName]] = { ...current };}return accumulator;}, {});// 将结果对象转换回数组result = Object.values(result);// 处理size为MB单位result.forEach((item: any) => {item.size = (item.size / 1024 / 1024).toFixed(2);});return result;
};//移除超出文件列表的项
const handleDelete = (rowIndex: number, row: any) => {fileData.goBeyondTable.splice(rowIndex, 1);fileData.allSize = (fileData.allSize - row.size).toFixed(2); //更新总大小MBfileData.fileSizeList = fileData.fileSizeList.filter((item: any) => item[fileData.firstFileName] !== row[fileData.firstFileName]);fileData.waitUploadList = fileData.waitUploadList.filter((item: any) => item[fileData.firstFileName] !== row[fileData.firstFileName]);
};/*请求上传到服务器*/
const handleUploadToServer = (clcik?: boolean) => {fileData.uploadList = fileData.waitUploadList;if (clcik && fileData.allSize > props.data.fileSize) {proxy.$message.error(`文件大小不能超过 ${props.data.fileSize} MB`);return;}let formData = new FormData();fileData.uploadList.forEach((item: any) => {formData.append(`${item.filePathName}`, item);});// // 遍历FormData对象并打印其内容:查看FormData对象数据是否正确// for (let [key, value] of formData.entries()) {// console.log(`${key}: ${value}`);// }emit('dragUploadAxiosFn', formData, fileData.uploadList); //上传参数抛出,外部做操作,不在组件做耦合
};//此方法:如果是文件夹,则会递归调用自己,所以最后都会走 else 的逻辑
const readFiles = async (item: any) => { if (item.isDirectory) {// 是一个文件夹const directoryReader = item.createReader();// readEntries是一个异步方法const entries: any[] = await new Promise((resolve, reject) => {directoryReader.readEntries(resolve, reject);});let files = [];for (const entry of entries) {const resultFiles: any = await readFiles(entry);files = files.concat(resultFiles);}return files;} else {// file也是一个异步方法const file = await new Promise((resolve, reject) => {item.file(resolve, reject);});let obj: any = getPath(item.fullPath); //通过路径获取名称方法changeFileSizeList(file, obj);return [file];}
};//更改 fileData.fileSizeList 的值:公共
const changeFileSizeList = (file: any, obj: any) => {file.filePathName = obj.filePathName; //添加路径名称file[fileData.firstFileName] = obj.pathObj[fileData.firstFileName]; //添加第一列文件名file.pathObj = obj.pathObj;let index = file.name.lastIndexOf('.');fileData.fileSizeList.push({ //添加图片路径、大小、名称filePathName: obj.filePathName,size: file.size,type: file.name.substring(index + 1),...obj.pathObj});
};//通过路径获取名称方法:公共
const getPath = (path: string) => {let filePathName: any = path; // 传给后端的全路径if (path.startsWith('/')) { // 如果路径以斜杠开头,则删除第一个斜杠filePathName = path.slice(1);}let parts = filePathName.split('/'); // 路径分割成数组let pathObj = {}; // 存储每个部分for (let i = 0; i < parts.length; i++) {if (parts[i] !== '') { // 跳过空字符串(如果路径以 / 开头或结尾)pathObj['pathName' + (i)] = parts[i];}}return {filePathName: filePathName,pathObj: pathObj}
};//关闭事件
const closeDialogFn = () => {if (fileData.step === 1) {props.data.visible = false; //关闭弹窗return;}proxy.$messageBox({title: '关闭',message: '关闭后不会保留您所做的更改,确定关闭吗?',callback: (value: string) => {//confirm=确认;cancel=取消if (value === 'confirm') {fileData.step = 1;props.data.visible = false; //关闭弹窗}}});
};
</script>
3、css 代码:
<style lang="scss" scoped>
.drag-box {position: relative;.progress-bar {position: absolute;z-index: 100;width: 100%;top: 0;left: 0px;right: 0px;bottom: -5px;display: flex;justify-content: center;align-items: center;background-color: rgba(255, 255, 255, 0.8);:deep(.el-progress.el-progress--line) {width: 100%;margin-left: 10px;}}.uploaded-list-wrap {max-height: 200px;overflow-y: auto;.uploaded-item {display: flex;justify-content: space-between;align-items: center;cursor: pointer;margin-bottom: 3px;.text-content {width: 80%;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;}.icon {width: 25px;height: 25px;}.success-icon {display: block;}.delete-icon {display: none;}&:hover {.success-icon {display: none;}.delete-icon {display: block;}}}}
}.div-text {width: 100%;height: 250px;border: 1px dashed #00b7ee;border-radius: 10px;box-sizing: border-box;display: flex;flex-direction: column;justify-content: center;align-items: center;font-size: 18px;.click-txt {color: #00b7ee;cursor: pointer;}.btn-wrap {margin-top: 20px;}
}.min-h311 {min-height: 311px;
}.max-h311 {max-height: 311px;
}:deep(.el-dialog .el-dialog__header) {padding: 12px 16px;
}:deep(.el-dialog .el-dialog__body) {padding: 15px 30px 20px;
}</style>
4、vue 页面中使用:
<!-- 拖拽上传 -->
<DragUpload v-if="dragUpload.visible":data="dragUpload"@dragUploadAxiosFn="dragUploadAxiosFn"
/>
const dragUpload: any = reactive({visible: false,fileSize: 100, // 单位字节 MBimgType: ['png', 'jpg', 'jpeg'], // 图片类型限制fileName: 'SPU', // 超出后列表展示的文件名
});
5、上传到后端接口的参数:
6、效果图,如下:
7、额外补充,后端接收文件流的方法:
相关文章:

Vue3+TS 实现批量拖拽文件夹上传图片组件封装
1、html 代码: 代码中的表格引入了 vxe-table 插件 <Tag /> 是自己封装的说明组件 表格列表这块我使用了插槽来增加扩展性,可根据自己需求,在组件外部做调整 <template><div class"dragUpload"><el-dialo…...
二叉树的所有路径(力扣257)
因为题目要求路径是从上到下的,所以最好采用前序遍历。这样可以保证按从上到下的顺序将节点的值存入一个路径数组中。另外,此题还有一个难点就是如何求得所有路径。为了解决这个问题,我们需要用到回溯。回溯和递归不分家,每递归一…...
Python OrderedDict 实现 Least Recently used(LRU)缓存
OrderedDict 实现 Least Recently used(LRU)缓存 引言正文 引言 LRU 缓存是一种缓存替换策略,当缓存空间不足时,会移除最久未使用的数据以腾出空间存放新的数据。LRU 缓存的特点: 有限容量:缓存拥有固定的…...

LabVIEW项目中的工控机与普通电脑选择
工控机(Industrial PC)与普通电脑在硬件设计、性能要求、稳定性、环境适应性等方面存在显著差异。了解这些区别对于在LabVIEW项目中选择合适的硬件至关重要。下面将详细分析这两种设备的主要差异,并为LabVIEW项目中的选择提供指导。 硬件设…...

Ansys Speos | Speos Meshing 网格最佳实践
概述 网格划分是在各种计算应用中处理3D几何的基本步骤: 表面和体积:网格允许通过将复杂的表面和体积分解成更简单的几何元素(如三角形、四边形、四面体或六面体)来表示复杂的表面和体积。 模拟和渲染:网格是创建离散…...
elasticsearch segment数量对读写性能的影响
index.merge.policy.segments_per_tier 是一个配置选项,用于控制 Elasticsearch 中段(segment)合并策略的行为。它定义了在每一层的段合并过程中,允许存在的最大段数量。调整这个参数可以优化索引性能和资源使用。 假设你有一个索…...

全同态加密理论、生态现状与未来展望(中2)
《全同态加密理论、生态现状与未来展望》系列由lynndell2010gmail.com和mutourend2010gmail.com整理原创发布,分为上中下三个系列: 全同态加密理论、生态现状与未来展望(上):专注于介绍全同态加密理论知识。全同态加密…...

鸿蒙UI(ArkUI-方舟UI框架)-开发布局
返回主章节 → 鸿蒙UI(ArkUI-方舟UI框架) 开发布局 1、布局概述 1)布局结构 2)布局元素组成 3)如何选择布局 声明式UI提供了以下10种常见布局,开发者可根据实际应用场景选择合适的布局进行页面开发。 …...

RPC是什么?和HTTP区别?
RPC 是什么?HTTP 是什么? 作为一个程序员,假设我们需要从A电脑的进程发送一段数据到B电脑的进程,我们一般会在代码中使用 Socket 进行编程。 此时,可选性一般就是 TCP 和 UDP 二选一,由于 TCP 可靠、UDP 不…...

Linux C\C++编程-建立文件和内存映射
【图书推荐】《Linux C与C一线开发实践(第2版)》_linux c与c一线开发实践pdf-CSDN博客 《Linux C与C一线开发实践(第2版)(Linux技术丛书)》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 Linu…...

行政纠错——pycorrector学习
pycorrector是一个开源中文文本纠错工具,它支持对中文文本进行音似、形似和语法错误的纠正。此工具是使用Python3进行开发的,并整合了Kenlm、ConvSeq2Seq、BERT、MacBERT、ELECTRA、ERNIE、Transformer等多种模型来实现文本纠错功能。pycorrector官方仓库…...

Go的defer原理
Go 的 defer 原理 defer 是 Go 语言中的一个关键字,用于延迟执行一个函数调用。它通常用于处理资源释放、连接关闭等操作,确保这些操作在函数返回之前执行。 1. 什么是 defer? defer 关键字用于延迟执行一个函数调用,直到包含它…...
Windows 下本地 Docker RAGFlow 部署指南
Windows 下本地 Docker RAGFlow 部署指南 环境要求部署步骤1. 克隆代码仓库2. 配置 Docker 镜像加速(可选)3. 修改端口配置(可选)4. 启动服务5. 验证服务状态6. 访问服务7. 登录系统8. 配置模型8.1 使用 Ollama 本地模型8.2 使用在线 API 服务9. 开始使用10. 常见问题处理端…...
专题三_穷举vs暴搜vs深搜vs回溯vs剪枝_全排列
dfs解决 全排列&子集 1.全排列 link:46. 全排列 - 力扣(LeetCode) 全局变量回溯 code class Solution { public:vector<vector<int>> ans;vector<int> cur;vector<bool> used;vector<vector<int>> permute…...

【IEEE Fellow 主讲报告| EI检索稳定】第五届机器学习与智能系统工程国际学术会议(MLISE 2025)
重要信息 会议时间地点:2025年6月13-15日 中国深圳 会议官网:http://mlise.org EI Compendex/Scopus稳定检索 会议简介 第五届机器学习与智能系统工程国际学术会议将于6月13-15日在中国深圳隆重召开。本次会议旨在搭建一个顶尖的学术交流平台…...

华为E9000刀箱服务器监控指标解读
美信监控易内置了数千种常见设备监测器,能够监测超过20万项指标。这些指标涵盖了从硬件设备到软件系统,从网络性能到安全状态等各个方面。如下基于美信监控易——IT基础监控模块,对华为E9000刀箱服务器部分监控指标进行解读。 一、华为E9000…...
【LC】2544. 交替数字和
题目描述: 给你一个正整数 n 。n 中的每一位数字都会按下述规则分配一个符号: 最高有效位 上的数字分配到 正 号。剩余每位上数字的符号都与其相邻数字相反。 返回所有数字及其对应符号的和。 示例 1: 输入:n 521 输出&…...

QT QTreeWidget控件 全面详解
本系列文章全面的介绍了QT中的57种控件的使用方法以及示例,包括 Button(PushButton、toolButton、radioButton、checkBox、commandLinkButton、buttonBox)、Layouts(verticalLayout、horizontalLayout、gridLayout、formLayout)、Spacers(verticalSpacer、horizontalSpacer)、…...

欧几里得算法求最小公倍数和最大公约数
一.最大公约数 gcd(a,b)gcd(b,a%b) 递归式,当且仅当b0,易得0和a的公约数为a.(可作为递归的出口) 证明: int gcd(int a, int b) {if (b 0) return a;else return gcd(b, a % b); } 二.最小公倍数 给定整数a b,求a b的最小公倍数 有图可知…...
Selenium配合Cookies实现网页免登录
文章目录 前言1 方案一:使用Chrome用户数据目录2 方案二:手动获取并保存Cookies,后续使用保存的Cookies3 注意事项 前言 在进行使用Selenium进行爬虫、网页自动化操作时,登录往往是一个必须解决的问题,但是Selenium每次…...

Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

vscode(仍待补充)
写于2025 6.9 主包将加入vscode这个更权威的圈子 vscode的基本使用 侧边栏 vscode还能连接ssh? debug时使用的launch文件 1.task.json {"tasks": [{"type": "cppbuild","label": "C/C: gcc.exe 生成活动文件"…...

linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
AspectJ 在 Android 中的完整使用指南
一、环境配置(Gradle 7.0 适配) 1. 项目级 build.gradle // 注意:沪江插件已停更,推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...

Python基于历史模拟方法实现投资组合风险管理的VaR与ES模型项目实战
说明:这是一个机器学习实战项目(附带数据代码文档),如需数据代码文档可以直接到文章最后关注获取。 1.项目背景 在金融市场日益复杂和波动加剧的背景下,风险管理成为金融机构和个人投资者关注的核心议题之一。VaR&…...

基于SpringBoot在线拍卖系统的设计和实现
摘 要 随着社会的发展,社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统,主要的模块包括管理员;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...
省略号和可变参数模板
本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...
MySQL 部分重点知识篇
一、数据库对象 1. 主键 定义 :主键是用于唯一标识表中每一行记录的字段或字段组合。它具有唯一性和非空性特点。 作用 :确保数据的完整性,便于数据的查询和管理。 示例 :在学生信息表中,学号可以作为主键ÿ…...