Vue3+Naive-ui+Jszip+Tailwind上传文件/文件夹/压缩包(包含拖拽文件上传功能)
一、前言
1、文中的class都是tailwind写法,不影响实际功能,可以用自己喜欢的样式书写
2、组件用的是naive-ui,不过只使用了下拉样式,可以用自己喜欢的下拉组件替换(比如element)
3、script用的是ts写法,可以用自己喜欢的语言写,大差不大
4、jszip和lodash不是自带的组件,需要npm安装
二、案列图示
三、代码
1、templete部分代码
<template>
<div class="w-full h-full flex flex-col items-start p-4 justify-center bg-[#efefef]">
<div class="my-5">
<input ref="Files" type="file" accept="text/plain" style="display: none;" @change="uploadFile" multiple>
<input ref="Folder" type="file" style="display: none;" @change="uploadFloder" webkitdirectory>
<input ref="Zip" type="file" accept=".zip" style="display: none;" @change="uploadZip">
<NDropdown trigger="hover" placement="bottom-start" :options="options1" @select="handleSelect">
<NButton type="primary">上传文件</NButton>
</NDropdown>
</div>
<div class="overflow-hidden h-full w-full" @dragover.prevent="handleDrag" @dragleave="handleDrag" @drop="handleDrop">
<div v-if="showUpload"
class="w-full h-full min-h-[calc(100vh - 620px)] flex items-center justify-center border-2 border-[#e0e0e6]">
<span class="text-[28px] text-[#000000CC] font-bold">拖到此处上传文件</span>
</div>
<NDataTable v-else :max-height="400" :columns="columns" :data="tableData" :bordered="false" striped></NDataTable>
</div>
</div>
</template>
2、上传文件
// 上传文件
function uploadFile() {
_.forEach(Files.value.files, (item: File) => {
filtration(item)
})
// 防止选择相同文件不触发 change 事件
Files.value.value = ''
}
3、上传文件夹
// 上传文件夹
function uploadFloder() {
_.forEach(Folder.value.files, (item: File) => {
filtration(item)
})
// 防止选择相同文件不触发 change 事件
Folder.value.value = ''
}
4、上传压缩包
// 上传压缩包(只支持zip)
function uploadZip(payload: Event | null, zip?: File) {
const file = zip ? zip : Zip.value.files[0]
if (file) {
// 筛选压缩包不能超过1G
let size = Number(file.size) / 1024 / 1024
if (file.type != 'application/x-zip-compressed') {
message.info(`有不是zip的文件夹-${file.name},已过滤`)
return
} else if (size > 1024) {
message.info(`有超过1G的压缩包-${file.name},已过滤`)
return
}
const reader = new FileReader();
reader.onload = function () {
const buffer = new Uint8Array(reader.result as ArrayBuffer);
jszip.loadAsync(buffer).then((zip) => {
const promises: Promise<void>[] = [];
zip.forEach((relativePath, zipEntry) => {
promises.push(new Promise((resolve, reject) => {
zipEntry.async('blob').then((blob) => {
const file = new File([blob], zipEntry.name);
filtration(file)
resolve();
}).catch((err) => {
reject(err);
});
}));
});
Promise.all(promises).then(() => {
console.log(tableData.value, '解压出来的文件');
}).catch((err) => {
console.error(err);
});
}).catch((err) => {
console.error(err);
});
};
reader.readAsArrayBuffer(file);
}
}
5、筛选文件(文件类型,文件大小,重复名)
// 筛选
function filtration(item: File) {
let size = Number(item.size) / 1024
if (!item.name.endsWith('txt')) {
message.info(`有不是txt的文件-${item.name},已过滤`)
} else if (size > 102400) {
message.info(`有超过100M的文件-${item.name},已过滤`)
} else if (_.findIndex(tableData.value, ['name', item.name]) != -1) { // 有重复名称的音频
message.info(`有相同名称的文件-${item.name},已过滤`)
} else {
if (tableData.value.length > 99) {
message.info(`一次性最多上传100条,超出的已过滤`)
} else {
tableData.value.push(item)
}
}
}
6、拖拽上传相关方法
// 防抖
const timeout = ref()
// 拖拽文件到指定区域上
function handleDrag(e: DragEvent) {
showUpload.value = true
// 防抖(在拖拽区域时由于dragover会不停触发,导致防抖启动,不会执行,只有在拖拽离开后dragover停止100ms后执行代码块)
if (timeout.value !== null) {
clearTimeout(timeout.value);
}
timeout.value = setTimeout(() => {
showUpload.value = false
timeout.value = null;
}, 100);
e.stopPropagation();
e.preventDefault();
}
// 拖拽文件到指定区域以后松开
function handleDrop(event: DragEvent) {
event.preventDefault();
const items = event.dataTransfer?.items;
_.forEach(items, (item: any) => {
const entry = item.webkitGetAsEntry();
// 如果是文件夹,则获取其中的文件列表
if (entry.isDirectory) {
readDirectory(entry)
} else {
var file = item.getAsFile()
if (file.type == 'application/x-zip-compressed') { // 压缩包
uploadZip(null, file)
} else {
filtration(file)
}
}
})
};
// 拿到文件夹里的文件
function readDirectory(entry: any) {
return new Promise((resolve, reject) => {
const reader = entry.createReader();
const list = ref<any>([])
const readEntries = () => {
reader.readEntries((entries: any) => {
if (entries.length > 0) {
entries.forEach((entry: any) => {
if (entry.isFile) { // 是文件
entry.file((file: File) => { filtration(file) });
}
});
readEntries();
} else {
resolve(list);
}
}, reject);
};
readEntries();
});
};
四、全部代码
<script setup lang="ts">
import { ref, h } from 'vue'
import { NButton, NDropdown, NDataTable, DataTableColumns, useDialog, useMessage } from 'naive-ui';
import jszip from 'jszip';
import _ from 'loadsh'
const dialog = useDialog()
const message = useMessage()
const showUpload = ref(false) // 是否显示拖拽上传框
const options1 = ref([
{ label: '文件上传', key: '1' },
{ label: '文件夹上传', key: '2' },
{ label: '压缩包上传', key: '3' }
])
const tableData = ref<File[]>([])
const createColumns = ({
deleteH,
}: {
deleteH: (row: File) => void
}): DataTableColumns<File> => {
return [
{ title: '文件名', key: 'name', ellipsis: true, },
{
title: '大小', key: 'size', ellipsis: true,
render(row) {
return fileSize(row.size)
},
},
{
title: '操作',
key: 'actions',
width: '120px',
render(row) {
return h('div', { class: 'flex space-x-4' }, [
h('span', {
class: 'cursor-pointer text-[#4DB319] hover:text-[#48ad40]',
onClick: () => deleteH(row),
}, '删除'),
])
},
},
]
}
const columns = createColumns({
// 删除
deleteH(rowData) {
dialog.create({
title: () => {
return (
h('div', { class: 'w-full h-full px-5 flex items-center' }, [
h('span', { class: 'text-[16px] text-[#4F4F4D]' }, '确定删除选中的数据吗?')
])
)
},
showIcon: false,
positiveText: "确定",
positiveButtonProps: {
type: 'primary',
size: 'medium'
},
onPositiveClick: () => {
const num = ref()
_.forEach(tableData.value, (item: File, index: number) => {
if (item.name == rowData.name) {
num.value = index
}
})
tableData.value.splice(num.value, 1)
},
negativeText: '取消',
negativeButtonProps: {
size: 'medium'
},
})
},
})
var Files = ref()
var Folder = ref()
var Zip = ref()
function handleSelect(key: string | number) {
switch (key) {
// 文件上传
case '1':
Files.value.click()
break;
// 文件夹上传
case '2':
Folder.value.click()
break;
// 压缩包上传
case '3':
Zip.value.click()
break;
default:
break;
}
}
function fileSize(size: number) {
let mb = Math.floor(size / (1024 * 1024));
let kb = Math.floor(size / 1024);
if (mb > 0) {
return (size / (1024 * 1024)).toFixed(2) + "MB";
} else if (kb > 0) {
return (size / 1024).toFixed(2) + "KB";
} else {
return size.toFixed(2) + "B";
}
}
// 筛选
function filtration(item: File) {
// 筛选
let size = Number(item.size) / 1024
if (!item.name.endsWith('txt')) {
message.info(`有不是txt的文件-${item.name},已过滤`)
} else if (size > 102400) {
message.info(`有超过100M的文件-${item.name},已过滤`)
} else if (_.findIndex(tableData.value, ['name', item.name]) != -1) { // 有重复名称的音频
message.info(`有相同名称的文件-${item.name},已过滤`)
} else {
if (tableData.value.length > 99) {
message.info(`一次性最多上传100条,超出的已过滤`)
} else {
tableData.value.push(item)
}
}
}
// 上传文件
function uploadFile() {
_.forEach(Files.value.files, (item: File) => {
filtration(item)
})
// 防止选择相同文件不触发 change 事件
Files.value.value = ''
}
// 上传文件夹
function uploadFloder() {
_.forEach(Folder.value.files, (item: File) => {
filtration(item)
})
// 防止选择相同文件不触发 change 事件
Folder.value.value = ''
}
// 上传压缩包(只支持zip)
function uploadZip(payload: Event | null, zip?: File) {
const file = zip ? zip : Zip.value.files[0]
if (file) {
// 筛选压缩包不能超过1G
let size = Number(file.size) / 1024 / 1024
if (file.type != 'application/x-zip-compressed') {
message.info(`有不是zip的文件夹-${file.name},已过滤`)
return
} else if (size > 1024) {
message.info(`有超过1G的压缩包-${file.name},已过滤`)
return
}
const reader = new FileReader();
reader.onload = function () {
const buffer = new Uint8Array(reader.result as ArrayBuffer);
jszip.loadAsync(buffer).then((zip) => {
const promises: Promise<void>[] = [];
zip.forEach((relativePath, zipEntry) => {
promises.push(new Promise((resolve, reject) => {
zipEntry.async('blob').then((blob) => {
const file = new File([blob], zipEntry.name);
filtration(file)
// tableData.value.push(file);
resolve();
}).catch((err) => {
reject(err);
});
}));
});
Promise.all(promises).then(() => {
console.log(tableData.value, '解压出来的文件');
}).catch((err) => {
console.error(err);
});
}).catch((err) => {
console.error(err);
});
};
reader.readAsArrayBuffer(file);
}
}
// 防抖
const timeout = ref()
// 拖拽文件到指定区域上
function handleDrag(e: DragEvent) {
showUpload.value = true
// 防抖(在拖拽区域时由于dragover会不停触发,导致防抖启动,不会执行,只有在拖拽离开后dragover停止100ms后执行代码块)
if (timeout.value !== null) {
clearTimeout(timeout.value);
}
timeout.value = setTimeout(() => {
showUpload.value = false
timeout.value = null;
}, 100);
e.stopPropagation();
e.preventDefault();
}
// 拖拽文件到指定区域以后松开
function handleDrop(event: DragEvent) {
event.preventDefault();
const items = event.dataTransfer?.items;
_.forEach(items, (item: any) => {
const entry = item.webkitGetAsEntry();
// 如果是文件夹,则获取其中的文件列表
if (entry.isDirectory) {
readDirectory(entry)
} else {
var file = item.getAsFile()
if (file.type == 'application/x-zip-compressed') { // 压缩包
uploadZip(null, file)
} else {
filtration(file)
}
}
})
};
// 拿到文件夹里的文件
function readDirectory(entry: any) {
return new Promise((resolve, reject) => {
const reader = entry.createReader();
const list = ref<any>([])
const readEntries = () => {
reader.readEntries((entries: any) => {
if (entries.length > 0) {
entries.forEach((entry: any) => {
if (entry.isFile) { // 是文件
entry.file((file: File) => { filtration(file) });
}
});
readEntries();
} else {
resolve(list);
}
}, reject);
};
readEntries();
});
};
</script>
<template>
<div class="w-full h-full flex flex-col items-start p-4 justify-center bg-[#efefef]">
<div class="my-5">
<input ref="Files" type="file" accept="text/plain" style="display: none;" @change="uploadFile" multiple>
<input ref="Folder" type="file" style="display: none;" @change="uploadFloder" webkitdirectory>
<input ref="Zip" type="file" accept=".zip" style="display: none;" @change="uploadZip">
<NDropdown trigger="hover" placement="bottom-start" :options="options1" @select="handleSelect">
<NButton type="primary">上传文件</NButton>
</NDropdown>
</div>
<div class="overflow-hidden h-full w-full" @dragover.prevent="handleDrag" @dragleave="handleDrag" @drop="handleDrop">
<div v-if="showUpload"
class="w-full h-full min-h-[calc(100vh - 620px)] flex items-center justify-center border-2 border-[#e0e0e6]">
<span class="text-[28px] text-[#000000CC] font-bold">拖到此处上传文件</span>
</div>
<NDataTable v-else :max-height="400" :columns="columns" :data="tableData" :bordered="false" striped></NDataTable>
</div>
</div>
</template>