my-drop/src/Drop.vue

<template>
  <div class="my-drop"
       :class="classes">
    <slot></slot>
  </div>
</template>

<script>
  /**
   * MyDrop 放置组件
   * @module $ui/components/my-drop
   */
  import bus from '$ui/utils/bus'
  import {on, off} from 'element-ui/lib/utils/dom'

  /**
   * 插槽
   * @member slots
   * @property {string} default 默认插槽,定义内容
   */

  export default {
    name: 'MyDrop',
    /**
     * 属性参数
     * @member props
     * @property {String|Array} [accept] 可放置的拖拽实例名称,与MyDrag的group参数值对应
     * @property {Boolean} [disabled] 是否禁用
     * @property {String} [activeClass] 激活时自定义样式
     * @property {String} [enterClass] 拖拽进入时自定义样式
     * @property {Boolean} [activeHighlight] 激活时高亮
     * @property {Boolean} [enterHighlight] 进入时高亮
     * @property {Boolean} [prevent] 是否阻止放置事件传播, my-drop 允许嵌套,为true时,子组件触发drop,不会传播给父组件
     */
    props: {
      // 设置可放置的draggable分组名称,字符串或数组
      accept: [String, Array],
      // 是否禁用
      disabled: Boolean,
      // 激活时自定义样式
      activeClass: String,
      // 拖拽进入时自定义样式
      enterClass: String,
      // 激活时高亮
      activeHighlight: Boolean,
      // 进入时高亮
      enterHighlight: Boolean,
      // 是否阻止放置事件传播
      prevent: Boolean

    },
    data() {
      this.dropChildren = []
      return {
        enter: false,
        rect: null,
        dragging: null
      }
    },
    computed: {
      acceptDragArray() {
        return this.accept ? [].concat(this.accept) : []
      },
      classes() {
        return {
          'is-active': this.active,
          'is-disabled': this.disabled,
          'is-enter': this.enter,
          'my-drop--active-highlight': (this.active && this.activeHighlight),
          'my-drop--enter-highlight': (this.enter && this.enterHighlight),
          [`${this.activeClass}`]: (this.activeClass && this.active),
          [`${this.enterClass}`]: (this.enterClass && this.enter)
        }
      },
      active() {
        // 禁用,不激活
        if (this.disabled) return false
        // 无拖拽,不激活
        if (!this.dragging) return false
        const group = this.dragging.group
        // 有拖拽,拖拽没设置分组名称,并且 放置没设置接收,即激活
        if (!group && this.acceptDragArray.length === 0) return true
        // 有拖拽,放置设置的接收包含拖拽分组名称,即激活
        return this.acceptDragArray.includes(group)
      }
    },
    methods: {
      handleDragStart(vm) {
        if (this.disabled) return

        this.dragging = vm
        this.setRect()
      },
      handleDragDragging(vm) {
        // 未进入,如果拖拽元素在可放置范围内,即时进入
        if (!this.enter && this.validate(vm)) {
          this.enter = true
          /**
           * 拖拽进入放置区时触发
           * @event enter
           * @param {VueComponent} drag 拖拽实例
           * @param {VueComponent} drop 放置实例
           */
          this.$emit('enter', this.dragging, this)
        }

        // 已进入,如果拖拽元素不在可放置范围内,即离开
        if (this.enter && !this.validate(vm)) {
          this.enter = false
          /**
           * 拖拽离开放置区时触发
           * @event leave
           * @param {VueComponent} drag 拖拽实例
           * @param {VueComponent} drop 放置实例
           */
          this.$emit('leave', this.dragging, this)
        }

        // 已进入 触发 move
        if (this.dragging && this.validate(vm)) {
          /**
           * 在放置区移动时触发
           * @event over
           * @param {VueComponent} drag 拖拽实例
           * @param {VueComponent} drop 放置实例
           */
          this.$emit('over', this.dragging, this)
        }
      },
      handleDragStop(vm) {
        // 触发放置
        if (this.enter && this.dragging) {
          // 设置 my-drag 成功放置
          vm.dropped = true
          /**
           * 放置时触发
           * @event drop
           * @param {VueComponent} drag 拖拽实例
           * @param {VueComponent} drop 放置实例
           */
          this.$emit('drop', this.dragging, this)
          this.enter = false
        }
        this.dragging = null
        this.rect = null
      },
      setRect() {
        if (this.rect) return
        this.rect = this.$el.getBoundingClientRect()
      },
      /**
       * 检测拖拽元素是否在可放置范围内
       * @param x
       * @param y
       * @return {boolean}
       */
      validate({clientX, clientY}) {
        if (!this.active) return false
        if (!this.rect) return false
        const matchChild = this.dropChildren.find(item => item.enter)
        // 如果已进入子组,父组件标识离开
        if (this.prevent && matchChild) {
          return false
        } else {
          const {left, top, width, height} = this.rect
          return clientX > left && clientX < left + width && clientY > top && clientY < top + height
        }


      },
      bindEventBus() {
        bus.$on('my-drag:start', this.handleDragStart)
        bus.$on('my-drag:dragging', this.handleDragDragging)
        bus.$on('my-drag:stop', this.handleDragStop)
      },
      unbindEventBus() {
        bus.$off('my-drag:start', this.handleDragStart)
        bus.$off('my-drag:dragging', this.handleDragDragging)
        bus.$off('my-drag:stop', this.handleDragStop)
      },
      getParent() {
        let parent = this.$parent
        while (parent && parent.$options.name !== 'MyDrop') {
          parent = parent.$parent
        }
        return parent
      }
    },
    created() {
      this.parent = this.getParent()
      // 在父组件中引用自己
      if (this.parent) {
        this.parent.dropChildren.push(this)
      }
      this.bindEventBus()
    },
    mounted() {
      this.document = window.document
      on(this.document, 'mousemove', this.handleMouseMove)
    },
    beforeDestroy() {
      this.unbindEventBus()
      if (this.document) {
        off(this.document, 'mousemove', this.handleMouseMove)
        this.document = null
      }
      // 在父组件中清除自己的引用
      if (this.parent) {
        this.parent.dropChildren = this.parent.dropChildren.filter(item => item !== this)
      }

    }
  }
</script>