跳转到内容

拖拽

可在移动端和pc端使用的拖拽组件,分为两个组件:DraggableDroppable

  • Draggable:表示一个可拖动元素。
  • Droppable:表示一个可放置拖动元素的区域。

为适应各种使用场景:

  • Draggable可以配置其拖拽类型type
  • Droppable可配置允许拖拽的类型acceptDragType
  • Droppable 只接受 type 在其 acceptDragType 中的 Draggable

安装

INFO

如果还需要使用其他组件,请参考全量组件安装

如果仅需要使用当前组件,执行以下命令单独安装:

shell
npm install @havue/drag-and-drop --save
shell
yarn add @havue/drag-and-drop
shell
pnpm install @havue/drag-and-drop

引入

vue
<script>
import { HvDraggable, HvDroppable } from 'havue'
// or 
import { HvDraggable, HvDroppable } from '@havue/components'
// or
import { HvDraggable, HvDroppable } from '@havue/drag-and-drop'
</script>

如果你需要在任意地方全局监听拖拽过程中的事件,也可以直接引入全局拖拽管理器:

ts
import { DnDManagerInstance } from '@havue/drag-and-drop'

示例

  • 左侧标签块为 Draggable 元素,文本显示其类型和大小。
  • 灰色 Draggable 元素设置了 immediate="right",可向右立即拖动。
  • yellow 标签块设置了 drag-item 自定义插槽展示。
  • 右侧有两个 Droppable 区域:green 标签块只能拖放到 green 区域,yellow 标签块只能拖放到 yellow 区域。

Draggable list:

green: 186 * 74
immediate: right
yellow: 177 * 77
green: 196 * 61
immediate: right
yellow: 163 * 72
yellow: 153 * 89
immediate: right
yellow: 199 * 55
green: 188 * 50
immediate: right
yellow: 179 * 86
yellow: 195 * 50
immediate: right
green: 162 * 53
green: 154 * 71
immediate: right
green: 182 * 93
yellow: 191 * 96
immediate: right
yellow: 187 * 64
green: 168 * 69
immediate: right
yellow: 181 * 80
green: 184 * 60
immediate: right
yellow: 189 * 51
green: 182 * 97
immediate: right
yellow: 157 * 88
yellow: 153 * 82
immediate: right
green: 188 * 56
green: 182 * 55
immediate: right
green: 190 * 69
green: 160 * 58
immediate: right
green: 166 * 59
yellow: 159 * 57
immediate: right
yellow: 167 * 77
yellow: 199 * 58
immediate: right
yellow: 193 * 75
green: 150 * 94
immediate: right
green: 160 * 99
green: 164 * 97
immediate: right
yellow: 197 * 71
yellow: 183 * 53
immediate: right
yellow: 191 * 59
green: 180 * 58
immediate: right
green: 192 * 98
green: 188 * 94
immediate: right
green: 160 * 95

Droppable's accept-drag-type property is set to 'green'.

Droppable's accept-drag-type property is set to 'yellow'.

点我看代码
vue
<template>
  <div class="dnd-demo" ref="divRef">
    <div class="left-box">
      <p>Draggable list:</p>
      <div class="top-drag-list-box">
        <Draggable :type="item.type" v-for="(item, i) in DragItems" :data="item" :key="i" :immediate="item.immediate">
          <div class="drag-box" :style="item.style">
            {{ item.type }}: {{ `${item.width} * ${item.height}` }}
            <br />
            {{ item.immediate ? 'immediate: ' + item.immediate : '' }}
          </div>
          <template #drag-item v-if="item.type === 'yellow'">
            <div style="width: 50px; height: 50px; background-color: yellow"></div>
          </template>
        </Draggable>
      </div>
    </div>

    <div class="right-box">
      <p>Droppable's accept-drag-type property is set to 'green'.</p>
      <Droppable
        accept-drag-type="green"
        @enter="
          (type: DragAndDropDragType, point: DragAndDropPoint, data: any) => {
            enteredType = 'green'
            onEnter(type, point, data)
          }
        "
        @move="onMove"
        @leave="onLeave"
        @drop="onDrop"
      >
        <div class="drop-box">
          <div class="preview-box" v-if="enteredType === 'green'" :style="previewStyle"></div>
          <div
            class="inner-box-item"
            v-for="item in greenInnerBoxList"
            :key="item.key"
            :style="{
              top: item.y,
              left: item.x,
              width: item.width,
              height: item.height
            }"
          ></div>
        </div>
      </Droppable>

      <p>Droppable's accept-drag-type property is set to 'yellow'.</p>
      <Droppable
        accept-drag-type="yellow"
        @enter="
          (type: DragAndDropDragType, point: DragAndDropPoint, data: any) => {
            enteredType = 'yellow'
            onEnter(type, point, data)
          }
        "
        @move="onMove"
        @leave="onLeave"
        @drop="onDrop"
      >
        <div class="drop-box yellow">
          <div class="preview-box" v-if="enteredType === 'yellow'" :style="previewStyle"></div>
          <div
            class="inner-box-item"
            v-for="item in yellowInnerBoxList"
            :key="item.key"
            :style="{
              top: item.y,
              left: item.x,
              width: item.width,
              height: item.height
            }"
          ></div>
        </div>
      </Droppable>
    </div>
  </div>
</template>
ts
<script setup lang="ts">
import type { DragAndDropPoint, DragAndDropDragType } from '@havue/drag-and-drop'
import { computed, ref, reactive } from 'vue'
// import { HvDraggable as Draggable, HvDroppable as Droppable } from '@havue/drag-and-drop'
import { HvDraggable as Draggable, HvDroppable as Droppable } from '@havue/components'

type BoxType = {
  key: string | number | symbol
  x: string
  y: string
  width: string
  height: string
}

type EnteredType = 'green' | 'yellow' | ''

const divRef = ref<HTMLElement>()

const enteredType = ref<EnteredType>('')
const enteredPoint = ref({
  x: 0,
  y: 0
})
const previewData = ref()

const DragItems = reactive(
  Array(40)
    .fill(0)
    .map((_, i) => {
      let random1 = Math.round(Math.random() * 50)
      let random2 = Math.round(Math.random() * 50)
      return {
        type: random1 % 2 === 0 ? 'green' : 'yellow',
        width: random1 + 150,
        height: random2 + 50,
        immediate: i % 2 == 0 ? ('right' as const) : undefined,
        style: {
          background: i % 2 == 0 ? 'gray' : ''
        }
      }
    })
)

const greenInnerBoxList = reactive<BoxType[]>([
  {
    key: 1,
    x: '20px',
    y: '20px',
    width: '20px',
    height: '20px'
  },
  {
    key: 2,
    x: '100px',
    y: '30px',
    width: '50px',
    height: '200px'
  },
  {
    key: 3,
    x: '200px',
    y: '40px',
    width: '90px',
    height: '40px'
  }
])

const yellowInnerBoxList = reactive<BoxType[]>([
  {
    key: 4,
    x: '200px',
    y: '40px',
    width: '90px',
    height: '40px'
  }
])

const previewStyle = computed(() => {
  const preview = previewData.value || {}
  return {
    top: `${enteredPoint.value.y}px`,
    left: `${enteredPoint.value.x}px`,
    width: `${preview.width || 100}px`,
    height: `${preview.height || 50}px`
  }
})

function onEnter(type: DragAndDropDragType, point: DragAndDropPoint, data: any) {
  enteredPoint.value = point
  previewData.value = data
}

function onMove(type: DragAndDropDragType, point: DragAndDropPoint, data: any) {
  enteredPoint.value = point
  previewData.value = data
}

function onLeave() {
  enteredType.value = ''
  enteredPoint.value = { x: 0, y: 0 }
  previewData.value = undefined
}

function onDrop(type: DragAndDropDragType, point: DragAndDropPoint, data: any) {
  data = data || {}
  const { width = 100, height = 50 } = data
  if (enteredType.value === 'green') {
    greenInnerBoxList.push({
      key: Date.now(),
      x: `${point.x - width / 2}px`,
      y: `${point.y - height / 2}px`,
      width: `${width}px`,
      height: `${height}px`
    })
  } else if (enteredType.value === 'yellow') {
    yellowInnerBoxList.push({
      key: Date.now(),
      x: `${point.x - width / 2}px`,
      y: `${point.y - height / 2}px`,
      width: `${width}px`,
      height: `${height}px`
    })
  }
  enteredType.value = ''
  enteredPoint.value = { x: 0, y: 0 }
  previewData.value = undefined
}
</script>
scss
<style lang="scss" scoped>
.dnd-demo {
  display: flex;
  width: 100%;
  height: 700px;
  overflow: auto;
  background-color: rgb(20 177 177 / 41.6%);

  p {
    margin: 5px;
  }

  .left-box {
    width: 30%;
    height: 100%;
    margin-right: 15px;
    overflow: auto;
    background-color: rgb(162 101 22 / 69.4%);

    .top-drag-list-box {
      display: flex;
      flex-wrap: wrap;
      gap: 20px;
      justify-content: flex-start;
      padding: 10px;

      .drag-box {
        width: 130px;
        height: 50px;
        color: white;
        user-select: none;
        background-color: black;
      }
    }
  }

  .right-box {
    flex: 1;

    .drop-box {
      position: relative;
      width: 400px;
      height: 300px;
      overflow: hidden;
      background-color: rgb(11 104 28);

      &.yellow {
        background-color: rgb(188 149 21);
      }

      .preview-box {
        position: absolute;
        background-color: rgb(127 255 212 / 30%);
        transform: translate(-50%, -50%);
      }

      .inner-box-item {
        position: absolute;
        background-color: rgb(143 10 65 / 46.6%);
      }
    }
  }
}
</style>

Draggable 属性

属性名说明类型默认值
type拖拽元素类型DragAndDropDragType-
immediate?拖拽元素立即响应拖动的方向,默认需要长按拖动ImmediateType | ImmediateType[]-
disabled?是否禁用boolean-
data?拖拽时传给 Droppable 事件的数据,在 drop/enter/move/leave 的第三个参数中获取any
ts
type DragAndDropDragType: string | number | symbol

type ImmediateType = 'left' | 'right' | 'top' | 'bottom' | 'all' | undefined

Draggable 插槽

名称说明
default默认内容
drag-item拖动展示元素

Droppable 属性

属性名说明类型默认值
acceptDragType可接受拖拽元素类型DragAndDropDragType | Array<DragAndDropDragType>-
disabled? ^1.2.0是否禁用boolean-

Droppable 事件

事件说明参数类型
enterDraggable进入时触发(type: DragType, point: DragAndDropPoint, data: any) => void
moveDraggable在区域内拖动时触发(type: DragType, point: DragAndDropPoint, data: any) => void
dropDraggable放下时触发(type: DragType, point: DragAndDropPoint, data: any) => void
leaveDraggable离开时触发(type: DragType, data: any) => void
ts
DragAndDropPoint: {
  /** 在Droppable中的横坐标 */
  x: string
  /** 在Droppable中的纵坐标 */
  y: string
}

Droppable 插槽

名称说明
default默认内容

全局拖拽管理器 DnDManagerInstance ^1.2.2

DnDManagerInstance 是基于内部事件总线实现的全局拖拽管理器,会在 document.body 上监听鼠标 / 触摸事件,并对外派发一组拖拽生命周期事件,方便你在任意地方统一监听。

ts
import { onBeforeUnmount, onMounted } from 'vue'
import type { DnDManagerEvents } from '@havue/drag-and-drop'
import { DnDManagerInstance } from '@havue/drag-and-drop'

const handleMove: DnDManagerEvents['move'] = ({ type, data, point }) => {
  console.log('move', type, data, point)
}

onMounted(() => {
  DnDManagerInstance.on('move', handleMove)
})

onBeforeUnmount(() => {
  DnDManagerInstance.off('move', handleMove)
})

事件类型

ts
type DragAndDropPoint = {
  x: number
  y: number
}

type DragAndDropDragType = string | number | symbol

type DnDManagerEvents = {
  down: (p: DragAndDropPoint) => void
  'first-move': (p: DragAndDropPoint, e: MouseEvent | TouchEvent) => void
  start: (p: DragAndDropPoint) => void
  move: (params: { type: DragAndDropDragType; data: any; point: DragAndDropPoint }) => void
  end: (params: { type: DragAndDropDragType; data: any; point: DragAndDropPoint }) => void
}
事件名说明参数
down指针按下时触发,此时还未真正开始拖拽(point: DragAndDropPoint)
first-movedown 之后第一次移动时触发,用于判断是否开始拖拽(point: DragAndDropPoint, e: MouseEvent | TouchEvent)
start确认进入拖拽状态时触发(长按或位移达到阈值)(point: DragAndDropPoint)
move拖拽过程中持续触发({ type: DragAndDropDragType, data: any, point: DragAndDropPoint })
end拖拽结束(松开鼠标 / 触点,或被取消)时触发({ type: DragAndDropDragType, data: any, point: DragAndDropPoint })

DnDManagerInstance 事件监听示例

下面的示例展示了如何订阅这些事件并实时在页面上展示:

Drag the block and observe global events on the right.

Drag me
Drop area

DnDManagerInstance events:

Status: idle

点我看代码(DnDManagerInstance 示例)
vue
<template>
  <div class="dnd-manager-demo">
    <div class="left-box">
      <p>Drag the block and observe global events on the right.</p>
      <Draggable :type="'box'" :data="{ width: 120, height: 60 }">
        <div class="drag-source">Drag me</div>
      </Draggable>

      <Droppable accept-drag-type="box">
        <div class="drop-area">Drop area</div>
      </Droppable>
    </div>

    <div class="right-box">
      <p>DnDManagerInstance events:</p>
      <p class="status">
        <span>Status: {{ status }}</span>
        <button class="clear-btn" type="button" @click="clearLogs">Clear logs</button>
      </p>
      <ul class="log-list">
        <li v-for="(item, index) in logs" :key="index">
          {{ item }}
        </li>
      </ul>
      <p v-if="lastPoint">Last point: ({{ lastPoint.x }}, {{ lastPoint.y }})</p>
    </div>
  </div>
</template>
ts
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import type { DragAndDropPoint, DnDManagerEvents } from '@havue/drag-and-drop'
import { DnDManagerInstance } from '@havue/drag-and-drop'
import { HvDraggable as Draggable, HvDroppable as Droppable } from '@havue/components'

const status = ref('idle')
const lastPoint = ref<DragAndDropPoint | null>(null)
const logs = ref<string[]>([])

const addLog = (text: string) => {
  logs.value.unshift(text)
}

const clearLogs = () => {
  logs.value = []
  status.value = 'idle'
  lastPoint.value = null
}

const handleDown: DnDManagerEvents['down'] = (point) => {
  status.value = 'down'
  lastPoint.value = point
  addLog(`down: (${point.x}, ${point.y})`)
}

const handleFirstMove: DnDManagerEvents['first-move'] = (point) => {
  status.value = 'first-move'
  lastPoint.value = point
  addLog(`first-move: (${point.x}, ${point.y})`)
}

const handleStart: DnDManagerEvents['start'] = (point) => {
  status.value = 'start'
  lastPoint.value = point
  addLog(`start: (${point.x}, ${point.y})`)
}

const handleMove: DnDManagerEvents['move'] = ({ type, point }) => {
  status.value = `move (${String(type)})`
  lastPoint.value = point
  addLog(`move: type=${String(type)}, point=(${point.x}, ${point.y})`)
}

const handleEnd: DnDManagerEvents['end'] = ({ type, point }) => {
  status.value = `end (${String(type)})`
  lastPoint.value = point
  addLog(`end: type=${String(type)}, point=(${point.x}, ${point.y})`)
}

onMounted(() => {
  DnDManagerInstance.on('down', handleDown)
  DnDManagerInstance.on('first-move', handleFirstMove)
  DnDManagerInstance.on('start', handleStart)
  DnDManagerInstance.on('move', handleMove)
  DnDManagerInstance.on('end', handleEnd)
})

onBeforeUnmount(() => {
  DnDManagerInstance.off('down', handleDown)
  DnDManagerInstance.off('first-move', handleFirstMove)
  DnDManagerInstance.off('start', handleStart)
  DnDManagerInstance.off('move', handleMove)
  DnDManagerInstance.off('end', handleEnd)
})
</script>
scss
<style scoped lang="scss">
.dnd-manager-demo {
  display: flex;
  gap: 16px;
  padding: 12px;
  background-color: rgb(20 177 177 / 20%);

  .left-box {
    flex: 1;

    .drag-source {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 120px;
      height: 60px;
      margin-bottom: 16px;
      color: #fff;
      user-select: none;
      background-color: #333;
    }

    .drop-area {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 260px;
      height: 160px;
      color: #fff;
      background-color: rgb(11 104 28);
    }
  }

  .right-box {
    flex: 1;

    .status {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
      margin: 8px 0;
      font-weight: 600;
    }

    .clear-btn {
      padding: 2px 8px;
      font-size: 12px;
      color: #333;
      background-color: #fff;
      border: 1px solid #ddd;
      border-radius: 4px;
      cursor: pointer;
    }

    .log-list {
      max-height: 200px;
      padding-left: 16px;
      overflow: auto;
      font-size: 12px;
    }
  }
}
</style>