Skip to content
On this page

文件上传

可以点击/拖拽上传文件🔗element-plus

效果

点击

思路

1. 选择文件

只有 <input type="file"> 可以选择文件,但是input 标签是浏览器原生组件,无法修改样式,所以使用<input type="file" hidden>隐藏,也可以使用 display:none隐藏

在点击其他元素触发 input 的 change 事件,也有两种方式

1. 使用 label 标签,点击 label 触发 input 的 change 事件

html
<label for="input">   
     <span>选择文件 </span>
     <input id="input" type="file" 
     @change="changeFileInput" 
     hidden 
     :multiple="multiple" 
     ref="inputRef">
   </label>

2. 使用 $refs 获取元素,触发 change 事件

vue
<template>
     <div>
       <el-button @click="handleClick">选择文件</el-button>
       <input id="input" type="file" @change="changeFileInput" hidden :multiple="multiple" ref="inputRef">
     </div>
   </template>
   <script>
     const inputRef = ref(null)
     const handleClick = () => {
       inputRef.value!.click()
     }
   </script>

2. 获取文件

inputchange 事件中,有一个参数 (e.target as HTMLInputElement).files 代表了所选的文件

对所选的文件进行处理,添加唯一标识 uid(file.uid = Date.now()),便于以后删除

3. 展示文件

URL.createObjectURL 接收 file/blob 作为参数,返回一个 URL 对象,这个 URL 对象可以作为 <img>src

4. 拖拽上传

使用 @drop.prevent="dragFile"/@dragleave.prevent/@dragover.prevent 事件进行监听是否有文件拖拽到元素上,由于dragleave/dragover 的默认行为是打开文件,所以需要阻止默认行为

drop 事件中触发的 dragFile 方法中,使用 event.dataTransfer.files 获取所拖拽的文件,剩下的和 input 流程一样

5. 删除文件

主要是通过唯一标识 uid 进行删除,同时为了避免内存泄漏,需要使用 URL.revokeObjectURL 解除引用

ts
const deleteFile = (delfile: UploadFile) => {
   filesRef.value = filesRef.value.filter(file => delfile.uid !== file.uid)
   URL.revokeObjectURL(delfile.url)
 }

源码

vue
<template>
  <div>
    <el-switch v-model="isDrag" class="mb-4" active-text="拖拽" inactive-text="点击"></el-switch>
    <div class="
    drag
   
    " v-if="isDrag" @drop.prevent="dragFile" @dragleave.prevent @dragover.prevent>
      <span>+</span>
    </div>
    <!-- label -->
    <div for="input" v-else>
      <!-- <span>选择文件 </span> -->
      <el-button @click="handleClick">选择文件</el-button>
      <input id="input" type="file" @change="changeFileInput" hidden :multiple="multiple" ref="inputRef">
    </div>

    <transition-group class="list" tag="div">
      <div v-for="file in filesRef" :key="file.uid" class="item">
        <img :src="file.url" class="img">
        <p class="name">{{ file.name }} </p>
        <div class="close" @click="deleteFile(file)">
          <p></p>
        </div>
      </div>
    </transition-group>
  </div>
</template>
<script lang="ts" setup>
import { ref, shallowRef, watch } from 'vue';

export interface IUploadRawFile extends File {
  uid: number
}

export interface UploadFile {
  name: string
  size?: number
  uid: number
  url: string
  raw?: IUploadRawFile
}
const isDrag = ref(false);
const multiple = ref(false);
const limit = ref(Infinity);

const filesRef = ref<UploadFile[]>([])

// 限制 limit 数量
const onExceed = (file: File[], filesRef: UploadFile[]) => {
  console.log(file, filesRef)
}
// 上传之前
const onBeforeLoad = (file: IUploadRawFile) => {
  console.log(file)
  return true
}
// 上传成功
const onSuccess = (file: IUploadRawFile, filesRef: UploadFile[]) => {
  console.log(file, filesRef)
}

const dragFile = (e: DragEvent) => {
  if (!e.dataTransfer?.files) return;
  uploadFiles(Array.from(e.dataTransfer.files))
}

/**
 * 
 * @param e input 选择的 file
 */
const changeFileInput = (e: any) => {
  const files = (e.target as HTMLInputElement).files
  // 构建自己的对象
  if (!files) return
  uploadFiles(Array.from(files))
}

/**
 * @description 删除操作
 * @param delfile 要删除的文件
 */

const deleteFile = (delfile: UploadFile) => {
  filesRef.value = filesRef.value.filter(file => delfile.uid !== file.uid)
  URL.revokeObjectURL(delfile.url)
}


const uploadFiles = (files: File[]) => {
  if (files.length === 0) return;

  if (limit && filesRef.value.length + files.length > limit.value) {
    return onExceed(files, filesRef.value)
  }
  if (!multiple) {
    files = files.slice(0, 1)
  }

  for (const file of files) {
    const rawFile = file as IUploadRawFile;
    // 对原生 file 添加 uid
    rawFile.uid = genFileId();
    handleStart(rawFile)
    // 准备上传
    upload(rawFile);
  }
}

// 组装 file
const handleStart = (file: IUploadRawFile) => {
  let uploadFile: UploadFile = {
    size: file.size,
    url: URL.createObjectURL(file),
    raw: file,
    uid: file.uid,
    name: file.name
  }
  filesRef.value = [...filesRef.value, uploadFile]
}

const upload = (rawFile: IUploadRawFile) => {
  if (!onBeforeLoad) {
    return doUpload(rawFile)
  };

  let r = true;
  if (onBeforeLoad) {
    r = onBeforeLoad(rawFile)
    if (r) {
      doUpload(rawFile)
    }
  }
}

const doUpload = (file: IUploadRawFile) => {
  // 如果是 成功态的话不用 上传
  console.log("执行上传")
  onSuccess(file, filesRef.value)
}

const inputRef = shallowRef<HTMLInputElement>();

const handleClick = () => {
  // 清空 input 的value 值
  inputRef.value!.value = ''
  inputRef.value!.click()
}

/**
 * @return 生成唯一 uid
 */
function genFileId() {
  return Date.now()
}


</script>

<style lang="scss" scoped>
.drag {
  @apply h-[200px] aspect-square flex rounded border-2 border-dashed border-red-500 items-center justify-center cursor-pointer p-2 text-3xl
}

.list {
  @apply flex gap-7 mt-4;

  .item {
    @apply h-[200px] flex flex-col p-2 relative aspect-square rounded-sm border-dashed border-blue-400;

    .img {
      @apply object-contain h-full
    }

    .name {
      @apply truncate m-0 text-center text-green-600 mt-auto;
    }

    .close {
      @apply absolute inset-0 z-10 opacity-0 flex items-center justify-center text-4xl transition-opacity duration-300;

      p {
        @apply hidden cursor-pointer
      }
    }

    &:hover {
      @apply bg-white bg-opacity-80 border-red-300;

      .img,
      .name {
        @apply opacity-40;
      }

      .close {
        @apply opacity-100;

        p {
          @apply block
        }
      }
    }
  }
}
</style>