[前端]单文件上传组件

0 阅读2分钟

本文介绍了一个单文件上传前端组件,基于Vue3、ElementPlus,提供了组件源码及使用示例教程,可供参考和使用。

支持的功能:文件覆盖、限制文件类型、最大文件大小

组件源码

<!--
  * 单文件上传组件
  * 
  * Author: GFire
  * Date: 2025/01/16
-->
<template>
  <div>
    <el-upload
      ref="upload"
      :limit="1"
      :accept="props.accept"
      :on-exceed="handleExceed"
      :on-change="handleChange"
      :on-remove="handleRemove"
      :auto-upload="false"
    >
      <!-- 默认插槽,用于放置触发文件选择的元素,如按钮、文字等 -->
      <slot name="default"></slot>
      <template #tip>
        <div style="font-size: 12px; color: var(--el-color-info)">
          <div v-if="props.accept">支持的文件类型:{{ props.accept }}</div>
          <div v-if="props.maxFileSize">支持的最大文件大小:{{ props.maxFileSize.size + props.maxFileSize.unit }}</div>
        </div>
        <!-- 用户自定义提示内容插槽 -->
        <slot name="tip"></slot>
      </template>
    </el-upload>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { ElNotification, genFileId } from 'element-plus';
import type { UploadInstance, UploadProps, UploadRawFile, UploadFile } from 'element-plus';

type SizeUnit = 'KB' | 'MB' | 'GB';

const props = defineProps<{
  /**
   * 接受上传的文件类型,以文件后缀用逗号拼接的字符串,如:`.jpg,.txt,.xlsx`,不传则无限制
   */
  accept?: string;
  /**
   * 支持的最大文件大小,不传则无限制
   */
  maxFileSize?: { size: number; unit: SizeUnit };
}>();

const emit = defineEmits<{
  (event: 'fileChange', file?: File): void;
}>();

defineExpose({
  /**
   * 清空文件列表
   */
  clearFile() {
    upload.value!.clearFiles();
  },
});

const upload = ref<UploadInstance>();
let tempFile: UploadFile | undefined;

// 覆盖前一个文件
const handleExceed: UploadProps['onExceed'] = (files) => {
  upload.value!.clearFiles();
  const file = files[0] as UploadRawFile;
  file.uid = genFileId();
  upload.value!.handleStart(file);
};

function handleChange(uploadFile: UploadFile) {
  if (!isValidFile(uploadFile)) {
    // 文件不合法,回退
    rollback();
  } else {
    tempFile = uploadFile;
    emit('fileChange', uploadFile.raw);
  }
}

// 校验文件是否合法
function isValidFile(uploadFile: UploadFile) {
  if (!isValidFileType(uploadFile)) {
    ElNotification.error({
      title: '文件不合法',
      message: `文件类型不支持,需为:${props.accept}`,
      position: 'top-right',
    });
    return false;
  }

  if (!isValidFileSize(uploadFile)) {
    ElNotification.error({
      title: '文件不合法',
      message: `文件大小超过限制:${props.maxFileSize?.size} ${props.maxFileSize?.unit}`,
      position: 'top-right',
    });
    return false;
  }

  return true;
}

function rollback() {
  if (tempFile) {
    upload.value!.clearFiles();
    upload.value!.handleStart(tempFile.raw!);
  } else {
    upload.value!.clearFiles();
  }
}

function handleRemove() {
  tempFile = undefined;
  emit('fileChange', undefined);
}

const acceptTypes = props.accept?.split(',');
function isValidFileType(uploadFile: UploadFile) {
  // 无值,代表接受任意文件类型
  if (!acceptTypes) {
    return true;
  }

  const fileType = '.' + uploadFile.name.split('.').pop();
  for (let type of acceptTypes) {
    if (fileType === type) {
      return true;
    }
  }
  return false;
}

function isValidFileSize(uploadFile: UploadFile) {
  // 无值,代表文件大小无限制
  if (!props.maxFileSize) {
    return true;
  }

  let bytes = convertToBytes(props.maxFileSize.size, props.maxFileSize.unit);
  if (uploadFile.raw!.size > bytes) {
    return false;
  } else {
    return true;
  }
}

function convertToBytes(size: number, unit: SizeUnit) {
  const unitMapping = {
    KB: 1024,
    MB: 1024 * 1024,
    GB: 1024 * 1024 * 1024,
  };

  const multiplier = unitMapping[unit];
  if (multiplier) {
    return size * multiplier;
  } else {
    throw new Error('Unsupported unit. Please use KB, MB, or GB.');
  }
}
</script>

<style scoped></style>

使用示例

示例代码:

<template>
    <SingleFileUpload
      style="width: 300px"
      ref="fileUploadRef"
      accept=".md,.txt"
      :maxFileSize="{ size: 50, unit: 'KB' }"
      @fileChange="handleFileChange"
    >
      <el-button>选择文件</el-button>
      <template #tip> 请上传符合要求的文件 </template>
    </SingleFileUpload>
</template>

<script setup lang="ts">
import { reactive, ref } from 'vue';
import SingleFileUpload from '@/components/base/SingleFileUpload.vue';

const fileUploadRef = ref();
// 接收文件变更
function handleFileChange(file: File | undefined) {
  form.file = file;
}

const form = reactive({
  file: undefined as File | undefined,
});

function submitForm() {
  // 模拟提交表单
  console.log('提交表单:', form);

  // 清空文件
  fileUploadRef.value.clearFile();
}
</script>

代码解释:

  • accept=".md,.txt":指定只接受md、txt的文件
  • :maxFileSize="{ size: 50, unit: 'KB' }":指定支持的最大文件大小为50KB
  • @fileChange="handleFileChange":文件变化事件处理

显示效果:

image.png

选择文件,默认限制为提供的文件类型(md、txt):

image.png

选择文件后的效果:

image.png

当选择的文件大小超过限制,则提示异常:

image.png

当选择的文件类型不支持,则提示异常:

image.png