Skip to content
On this page

Upload 拖拽上传组件

本身UI组件el-upload都是支持拖拽上传的,但是由于我们的上传文件增加了几个动画帧大致是以下:

  • 默认是一个闭合的文件夹 (默认状态)
  • 一个文件夹张开的动画 (表示鼠标放上去、或者拖拽文件到容器里了);
  • 一个文件夹张开的图片(表示要上传文件)
  • 一个文件夹投递的动画 (表示上传中);
  • 一个是文件夹上传完成闭合的动画(表示上传完了);

以上几个动画帧组成一整个文件上传的动作。

现有的组件指定支持不了了,因为没有api暴漏上传的生命周期;

改造

也就是拿源码经行改造,支持点击上传、拖拽上传就好了。

定义了拖拽的生命周期:

js
export const DRAG_TYPE = {
  DEFAULT: 'default', // 默认
  BEFORE_DRAGOVER: 'before-dragover', // 拖拽/鼠标滑过前动画
  DRAGOVER: 'dragover', // 拖拽/鼠标滑过
  UNLOADING: 'uploading', // 上传中
  BEFORE_OVER: 'before-over', // 结束前
};

其实就是再某个阶段展示某个生命周期的图片资源。

拖拽碰到里面的元素会重置拖拽事件

当拖拽的文件碰到upload里面的dom 就会重置拖拽事件(drop、dragover、dragleave),导致界面发生闪动, 我再el-upload里面也测试了 也有同样的问题。 只不过他没有我的那些状态动画帧展示效果没有那么明显。

js
@drop.prevent="onDrop" // drop 是拖拽的文件到松手的范围 松手了的事件
@dragover.prevent="onOpen"  // dragover 是拖拽的文件到松手的范围的事件
@dragleave.prevent="onClose"  // dragleave 拖拽文件离开事件

解决方法就是把拖拽的容器定位 让他的层级更高,里面不要嵌套dom;

原本是:

vue
<template>
  <div
    class="upload-dragger-container"
    :class="{
      'is-active': currentUploadType !== DRAG_TYPE.DEFAULT,
    }"
    @drop.prevent="onDrop"
    @dragover.prevent="onOpen"
    @dragleave.prevent="onClose"
    @click="onClick"
    @mouseover="onMouseover"
    @mouseleave="onMouseleave"
  >
    <input
      ref="input"
      class="upload-dragger__input"
      type="file"
      :multiple="multiple"
      :accept="accept"
      @change="handleChange"
    >
    <slot></slot>
  </div>
</template>

经过改造:

vue
<template>
  <div
    class="upload-dragger-container"
    :class="{
      'is-active': currentUploadType !== DRAG_TYPE.DEFAULT,
    }"
  >
  <!-- 不让拖拽放置容器里面存在dom -->
    <div 
      class="upload-dragger__area"
      @drop.prevent="onDrop"
      @dragover.prevent="onOpen"
      @dragleave.prevent="onClose"
      @click="onClick"
      @mouseover="onMouseover"
      @mouseleave="onMouseleave"
    />
    <input
      ref="input"
      class="upload-dragger__input"
      type="file"
      :multiple="multiple"
      :accept="accept"
      @change="handleChange"
    >
    <slot></slot>
  </div>
</template>

完整代码

完整代码:

vue
<template>
  <div
    class="upload-dragger-container"
    :class="{
      'is-active': currentUploadType !== DRAG_TYPE.DEFAULT,
    }"
  >
    <div
      class="upload-dragger__area"
      @drop.prevent="onDrop"
      @dragover.prevent="onOpen"
      @dragleave.prevent="onClose"
      @click="onClick"
      @mouseover="onMouseover"
      @mouseleave="onMouseleave"
    />
    <input
      ref="input"
      class="upload-dragger__input"
      type="file"
      :multiple="multiple"
      :accept="accept"
      @change="handleChange"
    >
    <slot></slot>
  </div>
</template>

<script>
import { DRAG_TYPE, handlerAccept } from './options';

export default {
  name: 'UploadDragger',

  props: {
    accept: {
      type: String,
      default: '',
      required: false,
    },

    multiple: {
      type: Boolean,
      default: false,
      required: false,
    },

    onUploadCallback: {
      type: Function,
      required: true,
      default: () => {},
    },
  },

  data() {
    return {
      currentUploadType: DRAG_TYPE.DEFAULT,
      DRAG_TYPE,
      dragover: false,
      openTimer: null,
      colseTimer: null,
    };
  },

  beforeDestroy() {
    this.clearTimeoutAll();
  },

  methods: {
    onDrop(e) {
      if (this.currentUploadType !== DRAG_TYPE.UNLOADING) {
        const dropFiles = Array.from(e.dataTransfer.files);
        const files = this.accept ? handlerAccept(dropFiles, this.accept) : dropFiles;
        this.onUploadFile(files);
      }
    },

    async onUploadFile(files) {
      this.clearTimeoutAll();
      this.currentUploadType = DRAG_TYPE.UNLOADING;
      this.$emit('current-type', DRAG_TYPE.UNLOADING);
      await this.onUploadCallback(files);
      console.log('upload complate!!')
      this.currentUploadType = DRAG_TYPE.BEFORE_OVER;
      this.$emit('current-type', DRAG_TYPE.BEFORE_OVER);
      this.onClose();
    },

    onClick() {
      if (this.currentUploadType !== DRAG_TYPE.UNLOADING) {
        this.$refs.input.value = null;
        this.$refs.input.click();
      }
    },

    handleChange(ev) {
      const { files } = ev.target;
      if (!files) return;
      this.onUploadFile(Array.from(files));
    },

    onMouseleave() {
      this.onClose();
    },

    onMouseover() {
      this.onOpen();
    },

    onClose() {
      if (this.currentUploadType !== DRAG_TYPE.UNLOADING) {
        this.clearTimeoutAll();
        this.currentUploadType = DRAG_TYPE.BEFORE_OVER;
        this.$emit('current-type', DRAG_TYPE.BEFORE_OVER);
        this.colseTimer = setTimeout(() => {
          this.currentUploadType = DRAG_TYPE.DEFAULT;
          this.$emit('current-type', DRAG_TYPE.DEFAULT);
          this.dragover = false;
        }, 150);
      }
    },

    onOpen() {
      if (!this.dragover && this.currentUploadType !== DRAG_TYPE.UNLOADING) {
        this.clearTimeoutAll();
        this.currentUploadType = DRAG_TYPE.BEFORE_DRAGOVER;
        this.$emit('current-type', DRAG_TYPE.BEFORE_DRAGOVER);
        this.dragover = true;
        this.openTimer = setTimeout(() => {
          this.currentUploadType = DRAG_TYPE.DRAGOVER;
          this.$emit('current-type', DRAG_TYPE.DRAGOVER);
        }, 150);
      }
    },

    clearTimeoutAll() {
      clearTimeout(this.colseTimer);
      clearTimeout(this.openTimer);
    },
  },
};
</script>

<style lang="less" scoped>
.upload-dragger-container {
  position: relative;
  cursor: pointer;
  width: 100%;
  height: 100%;
  border: 1px dotted #3F4664;
  border-radius: 3px;

  .upload-dragger__input {
    display: none;
  }

  .upload-dragger__area {
    z-index: 9;
    position: absolute;
    width: 100%;
    height: 100%;
  }
  &.is-active {
    background-color: #1B2D48;
    border: 1px dotted #1B2D48;
  }
}

</style>
  • options.js
js
// options.js
export const DRAG_TYPE = {
  DEFAULT: 'default', // 默认
  BEFORE_DRAGOVER: 'before-dragover', // 拖拽/鼠标滑过前动画
  DRAGOVER: 'dragover', // 拖拽/鼠标滑过
  UNLOADING: 'uploading', // 上传中
  BEFORE_OVER: 'before-over', // 结束前
};

export const  handlerAccept = (files = [], accept) => {
  return files.filter((file) => {
    const { type, name } = file;
    const extension = name.indexOf('.') > -1
      ? `.${name.split('.').pop()}`
      : '';
    const baseType = type.replace(/\/.*$/, '');
    return accept.split(',')
      .map(type => type.trim())
      .filter(type => type)
      .some((acceptedType) => {
        if (/\..+$/.test(acceptedType)) {
          return extension === acceptedType;
        }
        if (/\/\*$/.test(acceptedType)) {
          return baseType === acceptedType.replace(/\/\*$/, '');
        }
        if (/^[^\/]+\/[^\/]+$/.test(acceptedType)) {
          return type === acceptedType;
        }
        return false;
      });
  });
};

使用方法

使用方法介绍

vue
<template>
 <upload-dragger
    accept=".xlsx"
    :on-upload-callback="onUpload"
    @current-type="onCurrentType"
  >
  <!-- 这个 upload-type 就是需要展示对应的动画帧的type-->
    <upload-type :upload-type="currentUploadType"/>
    <div class="upload-dragger__text">
      <div>仅支持上传.xlsx格式文件,文件大小:10M以内</div>
        <el-button
          type="primary"
          size="small"
        >
        请选择文件
        </el-button>
       </div>
    </upload-dragger>
</template>

<script>
import UploadDragger from '@/components/upload-dragger/index.vue';
import { DRAG_TYPE } from '@/components/upload-dragger/options';
import UploadType from '@/components/upload-dragger/upload-type.vue';

export default {
  name: 'App',

  components: {
    UploadDragger,
    UploadType,
  },

  data() {
    return {
      currentUploadType: DRAG_TYPE.DEFAULT,
    };
  },

  methods: {
    onCurrentType(val) {
      this.currentUploadType = val;
    },

    async onUpload(files) {
      const data = new FormData();

      if (files?.length) {
        files.forEach((element) => {
          data.append('file', element);
        });
      } else {
        this.$message.error('请确认文件格式!');
        return;
      }
     // 调用上传接口
      this.$message.success('上传成功');
    },
  },
};
</script>
  • upload-type 组件代码也奉上 图片资源就不提供了
vue
<template>
  <div class="upload-type-container">
    <img
      v-show="uploadType === DRAG_TYPE.DEFAULT"
      src="./images/1.png"
    >
    <img
      v-show="uploadType === DRAG_TYPE.BEFORE_DRAGOVER"
      src="./images/2.png"
    >
    <img
      v-show="uploadType === DRAG_TYPE.DRAGOVER"
      src="./images/3.png"
    >
    <img
      v-show="uploadType === DRAG_TYPE.UNLOADING"
      src="./images/4.png"
    >
    <img
      v-show="uploadType === DRAG_TYPE.BEFORE_OVER"
      src="./images/5.png"
    >
  </div>
</template>

<script>
import { DRAG_TYPE } from './options';

export default {
  name: 'UploadType',

  props: {
    uploadType: {
      type: String,
      default: DRAG_TYPE.DEFAULT,
      required: true,
    },
  },

  data() {
    return {
      DRAG_TYPE,
    };
  },
};
</script>

<style lang="less" scoped>
 .upload-type-container {
    position: absolute;
    top: 0;
    left: calc(50% - 172px);
    width: 320px;
    height: 240px;
}
</style>

Released under the MIT License.