一、html 代码:
代码中的表格引入了 vxe-table 插件
<Tag /> 是自己封装的说明组件
表格列表这块我使用了插槽来增加扩展性,可根据自己需求,在组件外部做调整
<template>
<div class="dragUpload">
<el-dialog v-model="data.visible"
width="600px"
center
:draggable="draggable"
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
:before-close="closeDialogFn"
>
<template #header>
<h3>{{ data.title || '拖拽上传' }}</h3>
</template>
<!-- 顶部:自定义插槽 -->
<slot name="topCustomize"></slot>
<!-- 文件上传区域 -->
<div class="drag-box"
v-if="fileData.step === 1"
@dragover="handleDragOver"
@dragleave="handleDragOver"
@drop="handleDrop"
>
<div class="div-text" >
<div class="drag-tip">
<div class="flex-content">
<i class="icon_fileUpload f-s60 color-409"></i>
</div>
拖拽文件至此区域上传
</div>
<div class="btn-wrap">
<el-button v-if="singleFile" @click="toUploadFile">选取文件</el-button>
<el-button v-if="batchfolder" @click="toUploadFloder">选取文件夹</el-button>
<input
v-if="singleFile"
:style="{ display: 'none' }"
type="file"
ref="fileUploadRef"
@change="handleFileChange"
multiple
/>
<input
:style="{ display: 'none' }"
type="file"
ref="fileUploadFloderRef"
@change="handleFloderChange"
webkitdirectory
multiple
/>
</div>
</div>
</div>
<!-- 根据外部传参,需要展示列表 -->
<div v-if="fileData.step !== 1">
<!-- 添加插槽,表格可在外部自定义展示 -->
<slot name="slotTable">
<div class="h300">
<vxe-table
:data="fileData.table"
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>
<!-- 外部传入,动态列表展示 -->
<template v-for="item in data.table">
<vxe-column :min-width="item.minWidth || 100" :title="item.title">
<template #default="{ row }">
{{ row[item.key] ? row[item.key] : '--' }} {{ item.unit ? item.unit : '' }}
</template>
</vxe-column>
</template>
<vxe-column width="100" title="操作" :visible="fileData.table.length > 1">
<template #default="{ $rowIndex, row }">
<el-tooltip
content="移除"
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>
</slot>
</div>
<!-- 自定义说明插槽:只在拖拽上传下展示 -->
<template v-if="fileData.step === 1">
<slot name="uploadTag"></slot>
</template>
<!-- 自定义说明插槽:只在有表格下展示 -->
<template v-if="fileData.step !== 1">
<slot name="tableTag">
<Tag class="m-t12"
:content="`上传文件总大小不能超过 ${data.fileSize} MB,当前文件总大小 ${fileData.allSize} MB`"
/>
</slot>
</template>
<!-- 底部按钮 -->
<template #footer v-if="!autoUpload || fileData.step !== 1">
<div class="dialog-footer">
<el-button @click="closeDialogFn">取消</el-button>
<!-- 开启手动上传则显示 -->
<el-button v-if="fileData.step === 1" type="primary" @click="handleUploadToServer">上传</el-button>
<!-- 需要再次提交的场景则显示,例:超出文件大小限制 -->
<el-button v-if="fileData.step !== 1 && uploadGoBeyond" type="primary" @click="handleUploadToServer(true)">提交</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
二、js 代码:
目前支持校验上传的文件类型有三种:
image:图片类型;video:视频类型;excel:表格类型(拓展了任意类型,但新增的类型中消息提示需要在外部传入)
这块主要思路是:将文件夹判断后进行递归,获取出文件夹中的文件出来,最后类似单个文件上传,然后将文件流进行遍历 append 进创建的 FormData 对象。具体方法看:readFiles() 和 handleUploadToServer()
添加拓展:
1、指定文件类型中的单个文件,限制上传的大小,单位字节 MB
2、限制指定类型的上传个数
3、限制指定某种类型总大小
3、图片分辨率校验
4、视频宽高比例校验
5、动态超出列表生成
<script lang="ts" setup>
import { reactive, ref, getCurrentInstance } from 'vue';
const { proxy }: any = getCurrentInstance();
// 该组件有四个插槽可使用
// 1、顶部:自定义插槽 <slot name="topCustomize">
// 2、表格:自定义表格插槽 <slot name="slotTable">
// 3、拖拽上传下展示的:说明标签 <slot name="uploadTag">
// 4、表格下展示的:说明标签 <slot name="tableTag">
// 组件参数配置
const props = defineProps({
// 重要参数配置
data: {
type: Object,
default: () => ({
// visible: false, // 是否显示对话框
// title: '', // 对话框标题,bubu不传默认:‘拖拽上传’
// fileSize: 100, // 文件大小限制,单位字节 MB,不传或为0则不限制
/*** type 对象为空或者不传,则不限制上传类型 */
// type: {
// 自定义上传的文件类型 = image:图片类型;video:视频类型;excel:表格类型;pdf:pdf文件类型
// 有需要可以自己追加上传类型,pdf: ['pdf'], 提示消息则需要自己在外部添加传入
// image: ['png', 'jpg', 'jpeg'],
// video: ['mp4', 'avi', 'mov'],
// excel: ['xlsx', 'xls']
// },
//
/*** 指定文件类型中的单个文件,限制上传的大小,单位字节 MB */
// singleFileSize: {
// image: 5,
// video: 10,
// excel: 10
// },
//
/*** 指定文件类型中的全部文件,限制上传的大小,单位字节 MB */
// allFileSize: {
// image: 5,
// video: 10,
// excel: 10
// },
//
/*** 限制指定类型的上传个数 */
// image[0]:最少上传个数,image[1]:最多上传个数;不限制最少或最多则传: [0, 10] 或者 [10, 0], 0 则代表不限制
// singleFileNumber: {
// image: [0, 10],
// video: [0, 10],
// excel: [0, 10]
// },
//
/*** 图片分辨率校验,不传或为0则不限制 */
// imageResolution: {
// minWidth: 0,
// minHeight: 0,
// maxWidth: 2000,
// maxHeight: 2000
// },
//
/*** 视频宽高比例校验,不传或为0则不限制 */
// videoProportion: {
// minWidth: 0,
// minHeight: 0,
// maxWidth: 2000,
// maxHeight: 2000
// }
/*** 内部需要展示的列表,例:可展示文件大小超出限制的列表 */
// table: [
// {
// title: '文件名', // 列标题
// key: 'pathName0', //展示的字段名
// minWidth: '180', // 列宽,不传默认100
// unit: 'MB' // 单位,不传则不显示单位
// }
// ],
//
// deleteFileType: ['db'], // 将上传文件中指定的后缀类型删除掉,不参与上传判断,避免上传类型校验不过的问题
// limit: 0, // 允许上传总文件的最大数量,0或不传默认无限制
}),
},
// 自定义消息提示,不传则使用组件内默认提示
message: {
type: Object,
default: () => {
/*** 格式错误自定义提示:根据 type 来,但格式需要写成 ${'type中的key'},因组件内部代码采用:查找 ${} 进行替换;不传则使用组件内默认提示 */
// formatMessage: {
// 'image': '图片只支持上传:${image} 格式',
// 'video': '视频只支持上传:${video} 格式',
// 'excel': 'Excel文件只支持上传:${excel} 格式'
// },
//
/*** 单个文件大小超出限制,消息提示:用法同上 ***/
// singleSizeMessage: {
// 'image': '单个图片大小不能超过 ${image} MB',
// 'video': '单个视频大小不能超过 ${video} MB'
// },
//
/*** 单个文件类型数量限制,消息提示:用法同上 ***/
// singleNumberMessage: {
// 'image-min': '最少上传 ${image-min} 张图片',
// 'video-min': '最少上传 ${video-min} 条视频',
// 'pdf-max': '最多上传 ${pdf-max} 个 PDF文件'
// },
//
/*** 指定文件类型总大小限制,消息提示 ***/
// allSizeMessage: {
// 'image': '图片文件总大小不能超过 ${image} MB',
// 'video': '视频文件总大小不能超过 ${video} MB',
// 'excel': 'Excel文件总大小不能超过 ${excel} MB',
// 'pdf': 'PDF文件总大小不能超过 ${excel} MB'
// }
}
},
// 是否开启,文件超出总大小后的列表展示,默认不开启
uploadGoBeyond: {
type: Boolean,
default: false
},
// 是否支持窗口拖拽,默认true
draggable: {
type: Boolean,
default: true
},
// 是否自动上传文件,默认true
autoUpload: {
type: Boolean,
default: true
},
// 是否支持打开 file 单文件上传,不传默认false
singleFile: {
type: Boolean,
default: false
},
// 是否支持打开 folder 文件夹上传,不传默认true
batchfolder: {
type: Boolean,
default: true
}
});
/**
* @param dragUploadAxiosFn 上传参数抛出,外部做处理,不与组件内部逻辑耦合
* @param dragUploadErrorTable 超出限制后,展示的列表插槽抛出,外部做处理,不与组件内部逻辑耦合
* @param dragUploadDeleteItem 删除文件列表项抛出,外部做处理,不与组件内部逻辑耦合
*/
const emit = defineEmits(['dragUploadAxiosFn', 'dragUploadErrorTable', 'dragUploadDeleteItem']);
const fileUploadRef = ref();
const fileUploadFloderRef = ref();
// 文件数据
const fileData: any = reactive({
step: 1, // 步骤: 1:文件拖拽上传;2:文件超出提示
uploadList: [], //上传的文件列表
waitUploadList: [], //存储待上传的文件列表
fileSizeList: [], //存储遍历出来文件里面所有的图片路径及大小
table: [], //内部列表展示
allSize: 0, //文件总大小 MB
firstFileName: 'pathName0' //第一列字段:key
});
// 组件内部默认拥有的消息提示数据
const messageData: any = reactive({
// 文件格式错误,消息提示
formatMessage: {
'image': '支持图片上传:${image} 格式',
'video': '支持视频上传:${video} 格式',
'excel': '支持Excel文件上传:${excel} 格式',
'pdf': '文件类型支持上传:${pdf} 格式'
},
// 单个文件大小超出限制,消息提示
singleSizeMessage: {
'image': '单个图片大小不能超过 ${image} MB',
'video': '单个视频大小不能超过 ${video} MB',
'excel': '单个Excel文件大小不能超过 ${excel} MB',
'pdf': '单个PDF文件大小不能超过 ${pdf} MB'
},
// 单个文件类型数量限制,消息提示
singleNumberMessage: {
'image-min': '最少上传 ${image-min} 张图片',
'video-min': '最少上传 ${video-min} 条视频',
'excel-min': '最少上传 ${excel-min} 个 Excel文件',
'pdf-min': '最少上传 ${pdf-min} 个 PDF文件',
'image-max': '最多上传 ${image-max} 张图片',
'video-max': '最多上传 ${video-max} 条视频',
'excel-max': '最多上传 ${excel-max} 个 Excel文件',
'pdf-max': '最多上传 ${pdf-max} 个 PDF文件'
},
// 指定文件类型总大小限制,消息提示
allSizeMessage: {
'image': '图片文件总大小不能超过 ${image} MB',
'video': '视频文件总大小不能超过 ${video} MB',
'excel': 'Excel文件总大小不能超过 ${excel} MB',
'pdf': 'PDF文件总大小不能超过 ${excel} MB'
}
});
/*文件上传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); // 文件总大小 MB
fileData.waitUploadList = filesList;
checkUploadFn(); //校验及上传
}
fileUploadRef.value.value = ''; //清空文件输入框的值,避免连续上传同一个文件时第二次无法触发到输入框的change事件
};
/*文件夹目录上传*/
const handleFloderChange = (e: any) => {
if (e.target.files) {
let filesList: any = Array.from(e.target.files);
fileData.allSize = 0;
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); // 文件总大小 MB
fileData.waitUploadList = filesList;
checkUploadFn(); //校验及上传
}
fileUploadFloderRef.value.value = ''; //清空文件输入框的值,避免连续上传同一个文件时第二次无法触发到输入框的change事件
};
// 拖放进入目标区域
const handleDragOver = (event: any) => {
event.preventDefault();
};
// 拖拽放置
const handleDrop = async (event: any) => {
event.preventDefault();
const promises: any[] = [];
for (const item of event.dataTransfer.items) {
const entry: any = item.webkitGetAsEntry();
if (entry.isDirectory && !props.batchfolder) {
proxy.$message.error(`只支持文件上传,不支持文件夹上传!`);
return;
}
if (!entry.isDirectory && !props.singleFile) {
proxy.$message.error(`只支持文件夹上传,不支持单个文件上传!`);
return;
}
promises.push(readFiles(entry));
}
const resultFilesArrays = await Promise.all(promises); // 等待所有文件读取完成
fileData.waitUploadList = resultFilesArrays.flat();
checkUploadFn(); //校验及上传
};
//文件读取完,针对文件的处理,校验和上传方法封装
const checkUploadFn = async () => {
deleteFileTypeFn();
let nextValue = await fileValidateFn(fileData.fileSizeList, fileData.waitUploadList);
if (!nextValue) return; // 校验方法
handleUploadToServer(); //上传文件到服务器
};
// 将上传文件中指定的后缀删除掉,不参与上传判断
const deleteFileTypeFn = () => {
if (props.data.deleteFileType) {
let deleteType = props.data.deleteFileType;
fileData.fileSizeList = fileData.fileSizeList.filter((item: any) => !deleteType.includes(item.suffix));
fileData.waitUploadList = fileData.waitUploadList.filter((item: any) => !deleteType.includes(item.suffix));
}
};
/**
* 文件校验函数
*
* @param fileSizeList 其他校验,如:文件类型、大小、数量等
* @param waitUploadList 目前用来校验图片分辨率
* @returns 验证通过返回true,否则返回false
*/
const fileValidateFn = async (fileSizeList: any, waitUploadList: any) => {
//文件类型判断和格式限制
if (props.data.type) {
if (!testingFileType(fileSizeList)) return false;
}
// 指定文件类型:单个文件限制上传的大小,限制指定类型上传个数,指定文件类型总大小限制
if (props.data.singleFileSize || props.data.singleFileNumber || props.data.allFileSize) {
if (!appointFileTypeFn(fileSizeList)) return false;
}
//文件数量超出限制
if (props.data.limit) {
if (!fileLimit(fileSizeList)) return false;
}
//总文件大小超出限制
if (props.data.fileSize) {
//开启:超出总大小后的列表展示
if (props.uploadGoBeyond && !allFileSizeLimit(fileSizeList)) {
fileData.table = getGoBeyondTable(fileSizeList);
fileData.step = 2;
return false;
}
//关闭:执行逻辑
if (!props.uploadGoBeyond && !allFileSizeLimit(fileSizeList)) {
return false;
}
}
// 图片分辨率校验 和 视频宽高比例校验
if (props.data.imageResolution || props.data.videoProportion) {
if (!await imageOrVideoFn(waitUploadList)) return false;
}
return true;
};
// 指定文件类型:单个文件限制上传的大小,限制指定类型上传个数,指定文件类型总大小限制
const appointFileTypeFn = (fileSizeList: any) => {
let singleFileSize = props.data.singleFileSize;
let singleFileNumber = props.data.singleFileNumber;
let allFileSize = props.data.allFileSize;
let sizeObj: any = {}; //存储超出大小的文件
let numberObj: any = {}; //存储各种类型文件的数量
let allSizeObj: any = {}; //存储各种类型文件的总大小
fileSizeList.forEach((item: any) => {
if (singleFileSize && singleFileSize[item.type]) {
let size = singleFileSize[item.type] * 1024 * 1024; // 转成 MB
// 找出超出的文件,并分类型,存入 sizeObj[key] 中
if (size < item.size) {
sizeObj[item.type] = sizeObj[item.type] || [];
sizeObj[item.type].push(item);
}
}
if (singleFileNumber) {
if (numberObj[item.type]) {
numberObj[item.type].push(item);
} else {
numberObj[item.type] = [];
numberObj[item.type].push(item);
}
}
if (allFileSize) {
if (allSizeObj[item.type]) {
allSizeObj[item.type] += item.size;
} else {
allSizeObj[item.type] = item.size;
}
}
});
// 指定文件类型中的单个文件,限制上传的大小
if (singleFileSize) {
//消息提示操作
let messageTemplates: any = messageData.singleSizeMessage;
// 外部传入:自定义提示信息替换成外部传入的
replaceMessage(messageTemplates, props.message?.singleSizeMessage);
if (Object.keys(sizeObj).length) {
let findKey: any = {}; // 找出需要提示的类型,例如:{ image: 5 } 表示图片类型超出限制了
for (let key in sizeObj) {
if (sizeObj.hasOwnProperty(key) && singleFileSize.hasOwnProperty(key)) {
findKey[key] = singleFileSize[key];
}
}
let message: string = '';
for (let key in findKey) {
if (findKey.hasOwnProperty(key) && messageTemplates.hasOwnProperty(key)) {
let template = messageTemplates[key];
// 使用正则表达式匹配 ${} 内的内容,并进行替换
message += template.replace(/\$\{([^}]+)\}/g, (_: any, match: any) => findKey[match]) + ';';
}
}
proxy.$message.error(message);
//复原,防止继续上传时叠加
fileData.fileSizeList = [];
sizeObj = {};
return false;
}
}
// 限制指定类型上传个数
if (singleFileNumber) {
const keysList = Object.keys(singleFileNumber);
let countObj: any = {}; // 存储需要校验的类型文件,例:{image-min: 8, pdf-max: 1}
keysList.forEach((key: string) => {
if (numberObj.hasOwnProperty(key)) {
let num = numberObj[key].length;
let range = singleFileNumber[key]; // 获取每种类型的限制范围
if (range[0] && num < range[0]) {
countObj[`${key}-min`] = range[0];
}
if (range[1] && num > range[1]) {
countObj[`${key}-max`] = range[1];
}
}
});
//消息提示
if (Object.keys(countObj).length) {
let messageTemplates: any = messageData.singleNumberMessage;
// 外部传入:自定义提示信息替换成外部传入的
replaceMessage(messageTemplates, props.message?.singleNumberMessage);
let message: string = '';
for (let key in countObj) {
if (countObj.hasOwnProperty(key) && messageTemplates.hasOwnProperty(key)) {
let template = messageTemplates[key];
// 使用正则表达式匹配 ${} 内的内容,并进行替换
message += template.replace(/\$\{([^}]+)\}/g, (_: any, match: any) => countObj[match]) + ';';
}
}
proxy.$message.error(message);
//复原,防止继续上传时叠加
fileData.fileSizeList = [];
numberObj = {};
return false;
}
}
// 指定文件类型总大小限制
if (allFileSize) {
const keysList = Object.keys(allFileSize);
let checkObj: any = {}; // 存储需要校验的类型文件,例:{ image: 0.1, video: 1 }
keysList.forEach((key: string) => {
let propsAllSize = allFileSize[key] * 1024 * 1024; // 转成 MB
let allSize = allSizeObj[key];
if (allSize > propsAllSize) {
checkObj[key] = allFileSize[key];
}
});
if (Object.keys(checkObj).length) {
let messageTemplates: any = messageData.allSizeMessage;
// 外部传入:自定义提示信息替换成外部传入的
replaceMessage(messageTemplates, props.message?.allSizeMessage);
let message: string = '';
for (let key in checkObj) {
if (checkObj.hasOwnProperty(key) && messageTemplates.hasOwnProperty(key)) {
let template = messageTemplates[key];
// 使用正则表达式匹配 ${} 内的内容,并进行替换
message += template.replace(/\$\{([^}]+)\}/g, (_: any, match: any) => checkObj[match]) + ';';
}
}
proxy.$message.error(message);
//复原,防止继续上传时叠加
fileData.fileSizeList = [];
allSizeObj = {};
return false;
}
}
return true;
};
/**
* 验证文件类型是否为允许的上传的类型
* @param fileSizeList 文件列表
* @param type image:图片类型;video:视频类型;excel:表格类型;pdf:pdf文件类型
* @param formatMessage 外部传入的自定义提示信息,替换默认提示信息或者新增进入提示信息
* @returns 如果文件类型不符合要求,返回 false;否则返回 true
*/
const testingFileType = (fileSizeList: any) => {
let type: any = props.data.type;
if (Object.keys(type).length === 0) return true; // type 对象为空或者不传,则不限制上传类型
// 使用 Object.values 获取对象值的数组,然后使用 flatMap 合并所有数组
const typeList = Object.values(type).flatMap((array: any) => array);
//过滤出不符合要求的文件类型
let filterList: any = fileSizeList.filter((item: any) => !typeList.includes(item.suffix));
if (filterList.length) {
// 消息提示
let messageTemplates: any = messageData.formatMessage;
// 外部传入:自定义提示信息替换成外部传入的
replaceMessage(messageTemplates, props.message?.formatMessage);
let message: string = '';
for (let key in type) {
if (type.hasOwnProperty(key) && messageTemplates.hasOwnProperty(key)) {
let template = messageTemplates[key];
// 使用正则表达式匹配 ${} 内的内容,并进行替换
message += template.replace(/\$\{([^}]+)\}/g, (_: any, match: any) => type[match].join(', ')) + ';';
}
}
proxy.$message.error(message);
//错误的文件,不管里面有没有正确格式的,一律清除
fileData.fileSizeList = [];
fileData.allSize = 0;
return false;
}
return true;
};
//文件数量超出限制
const fileLimit = (fileSizeList: any) => {
if (fileSizeList.length > props.data.limit) {
proxy.$message.error(`文件数量不能超过 ${props.data.limit} 个,请重新上传!`);
fileData.fileSizeList = [];
return false;
}
return true;
};
//总文件大小超出限制
const allFileSizeLimit = (fileSizeList: any) => {
let allSize = fileSizeList.reduce((accumulator: any, currentValue: any) => {
if (currentValue) {
return accumulator + currentValue.size;
}
return accumulator;
}, 0);
let fileSize = props.data.fileSize * 1024 * 1024;
fileData.allSize = (allSize / 1024 / 1024).toFixed(2); //存储文件总大小 MB
if (allSize > fileSize) {
proxy.$message.error(`文件总大小不能超过 ${props.data.fileSize} MB,请重新上传!`);
emit('dragUploadErrorTable', fileData);
return false;
}
return true;
};
// 图片分辨率校验 和 视频宽高比例校验 Start
const imageOrVideoFn = async (waitUploadList: any) => {
let imageError: any = []; //存储分辨率不符合要求的图片文件列表
let imageRules = props.data.imageResolution;
let videoError: any = []; //存储宽高比不符合要求的视频文件列表
let videoRules = props.data.videoProportion;
if (imageRules) {
let imageList = waitUploadList.filter((item: any) => item.customizeType === 'image'); //筛选出图片类型的文件列表
if (!imageList.length) return true;
for (let i = 0; i < imageList.length; i++) {
await imageValidFn(imageList[i], imageError, imageRules);
}
}
if (videoRules) {
let videoList = waitUploadList.filter((item: any) => item.customizeType === 'video'); //筛选选出视频类型的文件列表
if (!videoList.length) return true;
for (let i = 0; i < videoList.length; i++) {
await videoValidFn(videoList[i], videoError, videoRules);
}
}
if (imageError.length) {
let message: string = '';
if (imageRules.minWidth && !imageRules.maxWidth) {
message = `图片分辨率需在 ${imageRules.minWidth} x ${imageRules.minHeight} 以上,请重新上传!`;
} else if (!imageRules.minWidth && imageRules.maxWidth) {
message = `图片分辨率需在 ${imageRules.maxWidth} x ${imageRules.maxHeight} 以下,请重新上传!`;
} else if (imageRules.minWidth && imageRules.maxWidth) {
message = `图片分辨率需在 ${imageRules.minWidth} x ${imageRules.minHeight} ~ ${imageRules.maxWidth} x ${imageRules.maxHeight} 之间,请重新上传!`;
}
proxy.$message.error(message);
return false;
}
if (videoError.length) {
let message: string = '';
if (videoRules.minWidth && !videoRules.maxWidth) {
message = `视频宽高比需在 ${videoRules.minWidth} :${videoRules.minHeight} 以上,请重新上传!`;
} else if (!videoRules.minWidth && videoRules.maxWidth) {
message = `视频宽高比需在 ${videoRules.maxWidth} :${videoRules.maxHeight} 以下,请重新上传!`;
} else if (videoRules.minWidth && videoRules.maxWidth) {
message = `视频宽高比需在 ${videoRules.minWidth} :${videoRules.minHeight} ~ ${videoRules.maxWidth} :${videoRules.maxHeight} 之间,请重新上传!`;
}
proxy.$message.error(message);
return false;
}
return true;
};
const imageValidFn = (fileItem: any, errorList: any, rules: any) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (event) => {
const img: any = new Image();
img.onload = () => {
if (img.width < rules.minWidth || img.width > rules.maxWidth ||
img.height < rules.minHeight || img.height > rules.maxHeight) {
errorList.push(fileItem); // 图片分辨率不符合要求
resolve(false);
} else {
resolve(true);
}
};
img.onerror = () => {
errorList.push(fileItem); // 图片加载失败,视为分辨率错误
resolve(false);
};
img.src = (event.target as any).result;
};
reader.onerror = () => {
errorList.push(fileItem); // 文件读取失败,视为分辨率错误
resolve(false);
};
reader.readAsDataURL(fileItem);
});
};
const videoValidFn = (file: any, videoError: any, rules: any) => {
return new Promise((resolve) => {
const video = document.createElement('video');
video.src = URL.createObjectURL(file);
video.onloadstart = () => {
// 获取视频的元数据
video.addEventListener('loadedmetadata', () => {
// 计算宽高比
const aspectRatio = video.videoWidth / video.videoHeight;
// 定义宽高比的范围(9:16 到 16:9)
let { minWidth, minHeight, maxWidth, maxHeight } = rules;
// 传值限制的宽高比范围
const minAspectRatio = minWidth / minHeight;
const maxAspectRatio = maxWidth / maxHeight;
// 检查宽高比是否在范围内
if (aspectRatio >= minAspectRatio && aspectRatio <= maxAspectRatio) {
resolve(true);
} else {
videoError.push(file); // 视频宽高比不符合要求
resolve(false);
}
// 释放对象URL
URL.revokeObjectURL(video.src);
});
};
video.onerror = () => { //无法加载视频以检查其分辨率
videoError.push(file); // 视频宽高比不符合要求
resolve(false);
};
});
};
// 图片分辨率校验 和 视频宽高比例校验 End
// 操作数据:文件超出后,展示的列表
const getGoBeyondTable = (fileSizeList: any) => {
// 遍历,相同第一列为一项,size累加
let result: any = fileSizeList.reduce((accumulator: any, current: any) => {
if (accumulator[current[fileData.firstFileName]]) { //如果已经存在,则累加size
accumulator[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.table.splice(rowIndex, 1);
fileData.allSize = (fileData.allSize - row.size).toFixed(2); //更新总大小MB
emit('dragUploadDeleteItem', fileData.allSize);
fileData.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 = (click?: boolean) => {
let autoUpload: boolean = props.autoUpload;
if (click) autoUpload = true; //手动上传
if (!autoUpload) return; //不自动上传,则不执行
fileData.uploadList = fileData.waitUploadList;
// 再次提交时验证文件大小是否超出限制
if (click && 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}`);
// }
fileData.allSize = 0;
fileData.fileSizeList = [];
fileData.waitUploadList = [];
if (props.data.uploadGoBeyond) props.data.title = ''; //重置标题
fileData.step = 1;
emit('dragUploadAxiosFn', 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: any = [];
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) => {
try {
let index = file.name.lastIndexOf('.');
let suffix = file.name.substring(index + 1); //获取后缀
let type = file.type.split('/')[0]; // 获取文件类型
if (['xlsx', 'xls'].includes(suffix)) {
type = 'excel'; //更改文件类型
}
if (['pdf'].includes(suffix)) {
type = 'pdf'; //更改文件类型
}
file.filePathName = obj.filePathName; //添加路径名称
file[fileData.firstFileName] = obj.pathObj[fileData.firstFileName]; //添加第一列文件名
file.pathObj = obj.pathObj;
file.suffix = suffix; //类型后缀
file.customizeType = type; //给文件添加自定义类型,方便后续处理
fileData.fileSizeList.push({ //添加图片路径、大小、名称
filePathName: obj.filePathName,
size: file.size,
suffix: suffix,
type: type,
...obj.pathObj
});
} catch {
proxy.$message.error(`文件读取路径失败,请重新上传文件!`);
}
};
//通过路径获取名称方法:公共
const getPath = (path: string) => {
try {
let filePathName: any = path; // 传给后端的全路径
if (path.startsWith('/')) { // 如果路径以斜杠开头,则删除第一个斜杠
filePathName = path.slice(1);
}
let parts = filePathName.split('/'); // 路径分割成数组
let pathObj: any = {}; // 存储每个部分
for (let i = 0; i < parts.length; i++) {
if (parts[i] !== '') { // 跳过空字符串(如果路径以 / 开头或结尾)
pathObj['pathName' + (i)] = parts[i];
}
}
return {
filePathName: filePathName,
pathObj: pathObj
}
} catch {
proxy.$message.error(`文件读取路径失败,请重新上传文件!`);
}
};
//自定义消息提示替换方法:公共
const replaceMessage = (message: any, propsMessage: any) => {
if (propsMessage && Object.keys(propsMessage).length) {
for (const key in propsMessage) {
if (message.hasOwnProperty(key)) {
// 如果 message 中存在该键,则替换其值
message[key] = propsMessage[key];
} else {
// 不存在则增加进去
message[key] = propsMessage[key];
}
}
}
};
//关闭事件
const closeDialogFn = () => {
if (fileData.step === 1) {
if (props.data.uploadGoBeyond) props.data.title = ''; //重置标题
props.data.visible = false; //关闭弹窗
return;
}
proxy.$messageBox({
title: '关闭',
message: '关闭后不会保留您所做的更改,确定关闭吗?',
callback: (value: string) => {
//confirm=确认;cancel=取消
if (value === 'confirm') {
fileData.step = 1;
if (props.data.uploadGoBeyond) props.data.title = ''; //重置标题
props.data.visible = false; //关闭弹窗
}
}
});
};
</script>
三、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 #409effc2;;
border-radius: 10px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 18px;
background-color: #cccccc1c;
.click-txt {
color: #409effc2;;
cursor: pointer;
}
.btn-wrap {
margin-top: 20px;
}
}
.h300 {
height: 300px;
}
.dragUpload {
:deep(.el-dialog .el-dialog__header) {
padding: 12px 30px;
display: flex;
justify-content: space-between;
box-shadow: 0px 0px !important;
border-radius: 5px 5px 0px 0px !important;
}
:deep(.el-dialog .el-dialog__body) {
padding: 8px 30px 11px;
width: 540px !important;
min-width: 540px !important;
max-width: 540px !important;
}
:deep(.el-dialog .el-dialog__footer) {
padding: 6px 30px 14px;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
.color-409 {
color: #409effc2;;
}
</style>
四、vue 页面中使用:
<!-- 拖拽上传 -->
<DragUpload v-if="dragUpload.visible"
:data="dragUpload"
@dragUploadAxiosFn="dragUploadAxiosFn"
/>
const dragUpload: any = reactive({
visible: false,
fileSize: 100, // 单位字节 MB
type: { //定义上传的文件类型 = image:图片类型;video:视频类型;excel:表格类型
image: ['png', 'jpg', 'jpeg']
},
deleteFileType: ['db'],
table: [ //超出动态列表生成
{
title: 'SPU文件名',
key: 'pathName0',
minWidth: '180'
},
{
title: '大小',
key: 'size',
minWidth: '111',
unit: 'MB'
}
]
});