Vue3上传文件/文件夹/压缩包(包含拖拽实现)

一、前言

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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值