• 基于 Vue + Axios 封装上传组件,并支持拖拽文件


    一、Input File

    使用  type='file'  的 <input> 元素可以选择文件,基于此我们可以封装自定义的上传组件

    <input type="file"> 可以接收四个附加属性:

    accept: 字符串,允许选择的文件类型,如: ".jpg,.jpeg,.png,.gif" 

    multiple: 布尔值,是否允许多选

    capture: 字符串,可以调用设备默认的相机或录音机(移动端有效)

    <input type="file" accept="image/*" capture="camera">

    files: 已选择文件对象列表,通过 HTMLInputElement.files 获取和赋值

     

    但 <input type="file"> 包含按钮和文件列表两部分,如果给它添加宽高和背景色...

    <input type="file" class="upload-inner">
    
    <style type="text/css">
      .upload-inner {
           200px;
          height: 80px;
          background-color: #e3e3e3;
      }
    </style>

    这个标签的样式基本没救了...

    所以为了能用上更美观的上传控件,通常会选择隐藏真正的文件上传控件

    然后用其他标签来代替上传按钮,在点击事件中触发上传事件

     

    二、控件设计

    这是一个常见的上传控件样式,它的 HTML 可以这么设计:

    <!-- wrapper 组件 -->
    
    <template>
      <div class="upload-wrapper">
        <div class="upload-inputs">
          <div class="upload-bg"></div>
          <upload
            :accept="uploadConfig.accept"
          ></upload>
          <div class="tip">点击上传按钮,或拖拽文件到框内上传</div>
          <div class="tiny-tip">请选择不大于 10m 的文件</div>
        </div>
      </div>
    </template>

    上面是外层的 <wrapper /> 组件,关于上传控件的具体逻辑可以单独封装到 <upload /> 组件中

    <!-- upload 组件 -->
    
    <template>
      <div class="file-selector">
        <button class="selector-btn" @click="handleUpClick">
          <slot>选择文件</slot>
        </button>
        <input
          ref="input"
          class="file-selector-input"
          type="file"
          :multiple="multiple"
          :accept="accept"
          @change="handleFiles"
        />
      </div>
    </template>

    在 <upload /> 组件中,需要将 <input /> 控件隐藏掉,并用 <button class="selector-btn"> 模拟一个上传按钮样式

    <style lang="scss">
    .file-selector {
      margin-bottom: 16px;
    
      .selector-btn {
        background-color: #2976e6;
        color: #fff;
        padding: 6px 18px;
        border-radius: 4px;
        cursor: pointer;
    
        &:hover {
          background-color: rgba($color: #2976e6, $alpha: 0.8);
          transition: background 180ms;
        }
      }
    
      &-input {
        display: none;
      }
    }
    </style>

    然后通过 <button class="selector-btn"> 的上传事件来触发 <input> 的上传事件 (上面的 handleUpClick

    并监听 <input> 的 change 事件,处理所选文件 (上面的 handleFiles

    // 调用上传功能
    handleUpClick() {
      this.uploadFinished // 维护一个上传状态,上传过程中禁用上传按钮
        ? this.$refs.input.click()
        : console.warn('请等待当前文件全部上传完成');
    },
    handleFiles(e) {
      const files = e?.target?.files;
      this.readFiles(files);
    },

     

    三、处理文件

    上面的 hanldeFiles 调用了 readFiles 方法,这是处理文件的主要逻辑

    // 上传之前将文件处理为对象
    readFiles(files) {
      if (!files || files.length <= 0) {
        return;
      }
      for (const file of files) {
        const url = window.URL.createObjectURL(file);
        const obj = {
          title: file.name.replace(/(.[^.]*$)|[\_]/gi, ''), // 去掉文件后缀
          url,
          file,
          fileType: file.fileType,
          status: 0, // 状态 -> 0 等待中,1 完成, 2 正在上传,3 上传失败
          percent: 0, // 上传进度
        };
        // 提前在 data 中定义 list,用来保存需要上传的文件
        this.list.push(obj);
      }
      // 在 data 中定义 startIndex 初始值为 0,上传完成后更新,用于追加上传文件
      this.startUpload(this.startIndex);
    },

    然后是上传文件

    // 上传前需要校验文件
    checkFile(index) {
      const file = this.list[index];
    
      // 如果文件不存在,即全部文件上传完成
      if (!file) {
        this.uploadFinished = true;
       // 上传完成,向父组件抛出 success 事件
    this.$emit('success', this.list); // 清空上传控件中的值,保证 change 事件能正常触发 this.$refs.input.value = null;
    this.startIndex = index > 1 ? index - 1 : 0;
    return false; } // 校验是否已上传 if (`${file.status}` === "1") { this.startUpload(++index); return false; } // 校验文件大小 if (this.maxSize && file.file && file.file.size >= this.maxSize) { this.startUpload(++index); return false; } return true; },


    // 上传单个文件 startUpload(index) {
    if (!this.checkFile(index)) { return; } // 开始上传,维护上传状态 this.uploadFinished = false; const file = this.list[index].file; const fileObj = this.list[index]; // 创建 formData 用于提交 const data = new FormData(); data.append('userfile', file); axios({ url: this.url, // 上传接口,由 props 传入 method: 'post', data, withCredentials: true, cancelToken: this.source.token, // 用于取消接口请求 // 进度条 onUploadProgress: e => { if (fileObj.status === 1) { return; } // 已上传 // 限定最大值为 99% const p = parseInt((e.loaded / e.total) * 99); if (e.total) { fileObj.status = 2; // 正在上传 fileObj.percent = p; // 更新上传进度 } else { fileObj.status = 3; // 上传失败 } }, }) .then(response => { if (`${response.code}` === "200") { fileObj.status = 1; fileObj.percent = 100; } else { fileObj.status = 3; } }) .catch(e => { fileObj.status = 3; }) .finally(e => { this.startUpload(++index); }); // 上传完成 },

    上传的时候用到了 axios 的 onUploadProgress 来维护上传进度和当前文件的上传状态,用于开发上传中的样式

     

    还使用了 cancelToken 来取消上传,不过取消上传还需要提前在 data 中定义一个 source 变量

    source: axios.CancelToken.source(), // axios 取消请求

    所以最终 <upload /> 组件中的 data 为:

    data: () => ({
        list: [], // 已选择的文件对象
        uploadFinished: true, // 上传状态
        startIndex: 0, // 开始上传的下标,用于追加文件
        source: axios.CancelToken.source(), // axios 取消请求
    }),

     

    最后在组件中添加一个重置方法

    reset() { // 重置
        this.list = [];
        this.source.cancel();
        this.startIndex = 0;
        this.uploadFinished = true;
        this.$refs.input && (this.$refs.input.value = null);
     },

    上传组件的核心逻辑就完成了

     

    四、拖拽上传

    除了基本的点击按钮上传之外,还需要支持拖拽上传,这部分逻辑可以在 <wrapper /> 组件中完成

    先回顾一下组件的 HTML 结构

    <!-- wrapper 组件 -->
    <template>
      <div class="upload-wrapper">
        <div 
          :class="['upload-inputs', dragging ? 'dragging' : '']"
          ref="pickerArea"
        >
          <div class="upload-bg"></div>
          <upload
         ref="uploadBtn"
    :accept="uploadConfig.accept" ></upload> <div class="tip">点击上传按钮,或拖拽文件到框内上传</div> <div class="tiny-tip">请选择不大于 10m 的文件</div> </div> </div> </template>

    上面增加了一个 dragging 变量,用来控制拖拽文件时的样式,需要在 data 中定义

    当组件初始化的时候,需要对 ref="pickerArea" 绑定拖拽事件

    在拖拽过程中,通过维护 dragging 状态来更新热区样式,当拖拽结束后,调用 <upload /> 组件的上传功能

    bindEvents() {
      const dropbox = this.$refs.pickerArea;
      // 防止重复绑定事件,需要在 data 中初始化 bindDrop 为 false
      if (!dropbox || this.bindDrop) { return }
      // 绑定拖拽事件,在组件销毁时解绑
      dropbox.addEventListener("drop", this.handleDrop, false);
      dropbox.addEventListener("dragleave", this.handleDragLeave);
      dropbox.addEventListener("dragover", this.handleDragOver);
      this.bindDrop = true;
    },
    // 拖拽到上传区域
    handleDragOver(e) {
      e.stopPropagation();
      e.preventDefault();
      this.dragging = true;
    },
    // 离开上传区域
    handleDragLeave(e) {
      e.stopPropagation();
      e.preventDefault();
      this.dragging = false;
    },
    // 拖拽结束
    handleDrop(e) {
      e.stopPropagation();
      e.preventDefault();
      this.dragging = false;
      const files = e.dataTransfer.files;
     // 调用 <upload/> 组件的上传功能
    this.$refs.uploadBtn && this.$refs.uploadBtn.readFiles(files); },

    上面通过 addEventLister 添加了事件监听,所以需要在 beforeDestroy 生命周期中注销

    beforeDestroy() {
      // 组件销毁前解绑拖拽事件
      try {
        const dropbox = this.$refs.pickerArea;
        dropbox.removeEventListener("drop", this.handleDrop);
        dropbox.removeEventListener("dragleave", this.handleDragLeave);
        dropbox.removeEventListener("dragover", this.handleDragOver);
        this.bindDrop = false;
      } catch (e) {}
    },

    到这里一个上传组件就完成了,不过只有上传功能,在样式和交互上还有未尽之处

    比如拖拽的时候热区的样式,上传过程中拖拽按钮的禁用样式等。不过这些都维护了相应状态,通过这些状态来添加样式即可

    真正没有涉及到的是已选中的文件样式,包括上传的进度条、文件操作等等

    但在上面 <upload /> 组件中抛出了 this.list,其中的每个元素也维护了上传状态和上传进度,可以基于此来开发对应的交互

     

    收工~

  • 相关阅读:
    Lintcode423-Valid Parentheses-Easy
    Lintcode97-Maximum Depth of Binary Tree-Easy
    Lintcode175-Revert Binary Tree-Easy
    Lintcode469-Same Tree-Easy
    Leetcode480-Binary Tree Paths-Easy
    Lintcode481-Binary Tree Leaf Sum-Easy
    Lintcode482-Binary Tree Level Sum-Easy
    Lintcode376-Binary Tree Path Sum-Easy
    SQL
    Database
  • 原文地址:https://www.cnblogs.com/wisewrong/p/13591315.html
Copyright © 2020-2023  润新知