一、封装背景
常用的组件库element-ui和t-design都只有拖拽文件,没有实现文件夹拖拽。拖拽文件夹有个限制:正常情况下只能读取100个文件,下面代码通过递归遍历解决了该问题。
参考地址blog.csdn.net/m0_52009348…
二、效果图
三、实现
1.先配置好t-design组件库,参考官方文档Vue Next for Web | TDesign (tencent.com)
2.代码实现
<script setup lang="ts">
import { ref } from "vue";
import { BrowseIcon } from "tdesign-icons-vue-next";
import { MessagePlugin } from "tdesign-vue-next";
const fileArrPromiseResolve =
ref<(value: File[] | PromiseLike<File[]>) => void>();
const fileArrPromise = ref<Promise<File[]>>();
const dropType = ref(false);
const pictureBoxLock = ref(false);
const fileList = ref<Array<File>>([]);
const isShow = ref(true);
const initFileArrPromise = () => {
fileArrPromise.value = new Promise((resolve) => {
fileArrPromiseResolve.value = resolve;
});
};
// 文件拖拽dragenter进入
const handlePictureBoxDragenter = () => {
// 这里传入一个10ms的延迟锁,让dragleave在enter延迟后触发
pictureBoxLock.value = true;
setTimeout(() => {
pictureBoxLock.value = false;
}, 10);
dropType.value = true;
};
// 文件拖拽dragleave离开
const handlePictureBoxDragleave = () => {
// 多张图片拖拽进入的延迟锁时返回
if (pictureBoxLock.value) return;
dropType.value = false;
};
// 文件拖拽drop落下
const handlePictureBoxDrop = async (e: DragEvent) => {
dropType.value = false;
// 判断file是文件夹还是文件
const isDirectory = e.dataTransfer?.items[0].webkitGetAsEntry()?.isDirectory;
// 这里的file给了一个初始值,如果是单张或多张图片拖拽可以直接获取其files属性
let file: File[] = Array.from(e.dataTransfer?.files ?? []);
if (isDirectory) {
// 这里对promise进行一个初始化
initFileArrPromise();
// 这里获取到文件的一个file信息
const fileItems: FileSystemDirectoryEntry =
e.dataTransfer.items[0].webkitGetAsEntry() as FileSystemDirectoryEntry;
// 这里是对fileItems处理,返回的是所有文件的数组
file = (await getFileDirectory(fileItems)) as File[];
}
// 如果未能读取到想要的图片或者文件夹为空,都直接提示错误返回
if (!file || !file.length) {
MessagePlugin.error("图片上传错误");
return;
}
fileList.value = fileList.value.concat(file);
isShow.value = false;
};
// 这里对文件处理通过promise进行处理,为了确保文件夹中最后一个文件读取完成后再进行操作
const getFileDirectory = (fileItems: FileSystemDirectoryEntry) => {
return new Promise((resolve) => {
fileTypeLoop(fileItems, []);
fileArrPromise.value?.then((files: File[]) => {
resolve(files);
});
});
};
// 文件夹上传解析,进行了递归和类型划分,返回fileArr数组
const fileTypeLoop = (
fileItem: FileSystemFileEntry | FileSystemDirectoryEntry,
fileArr: File[],
loopOver = false
) => {
let dirReader: FileSystemDirectoryReader | null = null;
// 如果fileItem是文件而不是文件夹进入
if (fileItem.isFile) {
(fileItem as FileSystemFileEntry).file((file: File) => {
// fileTypeFilter方法是对你想要哪一类型的文件,我这里只想要图片格式
const fileFilter =
file.type &&
"image/gif,image/jpeg,image/jpg,image/png,image/bmp".indexOf(
file.type
) > -1;
if (fileFilter) {
// 重新创建file数据格式,加入type和path放入到fileArr中
const newFile = new File([file], file.name, { type: file.type });
fileArr.push(newFile);
}
// 如果文件夹读到最后一个时,将fileArr数组通过resolve返回出去
if (loopOver) fileArrPromiseResolve.value!(fileArr);
});
} else if (fileItem.isDirectory) {
// 如果fileItem是文件夹,读取文件夹内容再进行处理
dirReader = (fileItem as FileSystemDirectoryEntry).createReader();
dirReader.readEntries(onReadEntries);
}
// 通过递归解析出文件夹中所有文件,readEntries正常只能读出100个文件
function onReadEntries(entries: FileSystemEntry[]) {
for (let i = 0; i < entries.length; i++) {
// 判断是否是最后一个文件,是的话就让loopOver为true
if (
i === entries.length - 1 &&
!entries[i].isDirectory &&
entries.length < 100
)
loopOver = true;
// 进行递归处理
fileTypeLoop(entries[i] as FileSystemDirectoryEntry, fileArr, loopOver);
}
// 如果entries.length则说明文件中可能不止100个文件,这个时候需要继续嵌套读取
if (entries.length) dirReader?.readEntries(onReadEntries);
}
};
//图片地址转化
function getImageAddress(file: File) {
return window.URL.createObjectURL(file);
}
const delAllImages = () => {
fileList.value = [];
isShow.value = true;
};
</script>
<template>
<div class="image-container">
<div
v-if="isShow"
class="image-upload"
:class="[{ 'drop-border': dropType }]"
@dragenter.stop.prevent="handlePictureBoxDragenter()"
@dragleave.stop.prevent="handlePictureBoxDragleave"
@drop.stop.prevent="handlePictureBoxDrop($event)"
>
<SIcon :icon="'file-upload'" :size="50" />
<p>将文件/文件夹拖拽到此处</p>
</div>
<!-- 图片显示预览 -->
<div v-else class="image-list">
<div
v-for="img in fileList"
:key="img.name"
class="tdesign-demo-image-viewer__base"
>
<t-image-viewer :images="[getImageAddress(img)]">
<template #trigger="{ open }">
<div class="tdesign-demo-image-viewer__ui-image">
<img
alt="test"
:src="getImageAddress(img)"
class="tdesign-demo-image-viewer__ui-image--img"
/>
<div
class="tdesign-demo-image-viewer__ui-image--hover"
@click="open"
>
<span><BrowseIcon size="1.4em" /> 预览</span>
</div>
</div>
</template>
</t-image-viewer>
</div>
</div>
<div v-if="fileList.length" class="continue-upload">
<div class="image-continue-upload">
<div
class="image-upload image-upload-little"
:class="[{ 'drop-border': dropType }]"
@dragenter.stop.prevent="handlePictureBoxDragenter()"
@dragleave.stop.prevent="handlePictureBoxDragleave"
@drop.stop.prevent="handlePictureBoxDrop($event)"
>
<p>继续添加文件,将文件/文件夹拖拽到此处</p>
</div>
</div>
<div class="del-all-btn" @click="delAllImages()">
<SIcon :icon="'del-icon'" :size="16" />
<span>清空</span>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.image-container {
width: 600px;
height: 300px;
position: relative;
background-color: #f9f9f9;
.continue-upload {
width: 100%;
height: 60px;
background-color: #f2f2f2;
position: absolute;
bottom: 0;
display: flex;
.image-continue-upload {
flex: 1;
.image-upload-little {
background-color: #f2f2f2;
}
}
.del-all-btn {
width: 60px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
}
}
.image-upload {
width: 100%;
height: 100%;
background-color: #f9f9f9;
border: 1px dashed #dcdcdc;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&:hover {
border-color: #0052d9;
}
p {
color: #00000099;
font-family: "微软雅黑";
}
}
.drop-border {
border-color: #0052d9;
}
.image-list {
width: 100%;
height: calc(100% - 60px);
display: flex;
flex-wrap: wrap;
overflow-y: scroll;
}
.image-list::-webkit-scrollbar {
width: 8px;
}
.image-list::-webkit-scrollbar-thumb {
border-radius: 10px;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
background: #c7ddfc;
}
.image-list::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1);
border-radius: 0;
background: #c7ddfc;
}
.image-list::-webkit-scrollbar-track-piece {
background: #eff3f8;
}
//图片预览样式
.tdesign-demo-image-viewer__ui-image {
width: 100%;
height: 100%;
display: inline-flex;
position: relative;
justify-content: center;
align-items: center;
border-radius: var(--td-radius-small);
overflow: hidden;
}
.tdesign-demo-image-viewer__ui-image {
width: 100%;
height: 100%;
display: inline-flex;
position: relative;
justify-content: center;
align-items: center;
border-radius: var(--td-radius-small);
overflow: hidden;
}
.tdesign-demo-image-viewer__ui-image--hover {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 0;
opacity: 0;
background-color: rgba(0, 0, 0, 0.6);
color: var(--td-text-color-anti);
line-height: 22px;
transition: 0.2s;
}
.tdesign-demo-image-viewer__ui-image:hover
.tdesign-demo-image-viewer__ui-image--hover {
opacity: 1;
cursor: pointer;
}
.tdesign-demo-image-viewer__ui-image--img {
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
cursor: pointer;
position: absolute;
}
.tdesign-demo-image-viewer__ui-image--footer {
padding: 0 16px;
height: 56px;
width: 100%;
line-height: 56px;
font-size: 16px;
position: absolute;
bottom: 0;
color: var(--td-text-color-anti);
background-image: linear-gradient(
0deg,
rgba(0, 0, 0, 0.4) 0%,
rgba(0, 0, 0, 0) 100%
);
display: flex;
box-sizing: border-box;
}
.tdesign-demo-image-viewer__ui-image--title {
flex: 1;
}
.tdesign-demo-popup__reference {
margin-left: 16px;
}
.tdesign-demo-image-viewer__ui-image--icons .tdesign-demo-icon {
cursor: pointer;
}
.tdesign-demo-image-viewer__base {
width: 160px;
height: 100px;
margin: 10px;
border: 4px solid var(--td-bg-color-secondarycontainer);
border-radius: var(--td-radius-medium);
}
</style>