Skip to page content

useDropZone 组合式 API
v2.34+

useDropZone() composable 能把一个元素(或组件)变成从桌面拖入文件的投放目标:当拖拽悬停在该区域上时,它会通过响应式的 isOverDropZone 布尔值告诉你;当文件被放下时,它会通过响应式数组 acceptedDropZoneFiles 以及 onDrop 钩子把 File 对象交给你。

被放下的文件会经过与 QFile 和 QUploader 完全相同的校验(accept、maxFileSize、maxTotalSize、maxFiles 和 filter),未通过校验的文件也会以与这两个组件触发 @rejected 相同的方式被上报。

当你希望用自己的卡片、面板或整个页面来接收拖入的文件,而 QFile 的表单字段外观或 QUploader 的上传队列反而碍手碍脚时,就可以用它。如果你只需要拿到文件并给元素加上一个悬停样式类,那么 v-drop-zone 指令在模板里就能搞定。可以再搭配区域内某个按钮上的 useFilePicker composable,让键盘和触屏用户也能提供文件。

TIP

在 SSR 或 SSG 模式的服务端,什么都无法被放下:在客户端接管之前,isOverDropZone 始终保持为 false,acceptedDropZoneFiles 也始终为空。

在组件之外使用

该 composable 也可以在 setup() 之外调用:在 boot 文件、store 或普通模块中。此时没有可回退的组件根节点,也没有需要等待的挂载时机,所以你必须提供一个 target(一个元素,或指向元素的 ref / getter);此时该区域会立即开始监听,且不会自行释放:完成后请调用 stopDropZone()(或把响应式的 target 指向 null)。

语法

import { useTemplateRef } from 'vue'
import { useDropZone } from 'quasar'

setup () {
  const target = useTemplateRef('target') // an Element or a component

  const {
    isOverDropZone,
    acceptedDropZoneFiles,
    rejectedDropZoneFiles,
    resetDropZone,
    stopDropZone
  } = useDropZone({
    // all optional:
    target,                 // omit it to use the component's own root element
    disabled: false,        // stop accepting drops

    multiple: false,        // accept more than one file per drop
    accept: 'image/*,.pdf', // same format as the native "accept" attribute
    maxFileSize: 1048576,   // bytes
    maxTotalSize: 10485760, // bytes
    maxFiles: 5,
    filter (files) {        // keep only the files you return
      return files.filter(file => file.name.endsWith('.jpg'))
    },

    onDrop (files, evt) {},   // the accepted files of a drop, plus the Event
    onRejected (rejected) {}, // [{ failedPropValidation, file }, ...]
    onEnter (evt) {},         // a drag entered the zone
    onLeave (evt) {}          // it left the zone or got dropped
  })

  // ...
}
function useDropZone(
  options?: MaybeRefOrGetter<{
    target?: MaybeRefOrGetter<
      Element | ComponentPublicInstance | null | undefined
    >
    disabled?: boolean
    multiple?: boolean
    accept?: string
    maxFileSize?: string | number
    maxTotalSize?: string | number
    maxFiles?: string | number
    filter?: (files: readonly File[]) => readonly File[]
    onDrop?: (files: File[], evt: DragEvent) => void
    onRejected?: (rejected: QRejectedEntry[]) => void
    onEnter?: (evt: DragEvent) => void
    onLeave?: (evt: DragEvent) => void
  }>
): {
  isOverDropZone: Ref<boolean>
  acceptedDropZoneFiles: ShallowRef<File[]>
  rejectedDropZoneFiles: ShallowRef<QRejectedEntry[]>
  resetDropZone: () => void
  stopDropZone: () => void
}

// the same type as the entries of the QFile/QUploader "rejected" event
interface QRejectedEntry {
  failedPropValidation:
    | 'accept'
    | 'max-file-size'
    | 'max-total-size'
    | 'filter'
    | 'max-files'
    | 'duplicate'
  file: File
}

不提供 target 时,投放区域就是调用该 composable 的那个组件的根元素,以组件被挂载的那一刻为准。渲染为片段(fragment,即多个根节点)的组件没有可供监听的根元素,因此这种情况下必须提供一个 target。

当有东西被拖拽悬停在该区域上(包括其子元素)时,isOverDropZone 会变为 true;当拖拽离开该区域或被放下时,它会回到 false;在这两次状态转换时会分别以拖拽事件为参数调用 onEnter 和 onLeave。你可以用它来高亮该区域。如果在拖拽进行到一半时释放了区域(通过 disabled、切换 target 或调用 stopDropZone()),isOverDropZone 会回到 false,但不会调用 onLeave,因为此时没有可上报的拖拽事件。嵌套在另一个区域内部的区域会独占这些拖拽事件,因此外层区域感知不到发生在内层区域上的投放(并且会保持高亮,直到下一次拖拽离开它为止)。

不设置 multiple 时,只有第一个被放下的文件会被保留(其余文件不会作为被拒绝项上报),这与 QFile 的行为一致。被放下的文件夹不会被展开:它们会表现为没有类型的文件,从而被 accept 过滤掉。

acceptedDropZoneFiles 保存最近一次投放中通过校验的文件。如果某次投放的所有文件都被拒绝,acceptedDropZoneFiles 会保持不变;而 rejectedDropZoneFiles 始终反映最近一次投放的结果。resetDropZone() 会同时清空这两者。

每一次投放都会调用 onDrop,参数为通过校验的文件(如果没有任何文件通过,或者拖拽根本没有携带文件,则为空数组)以及投放事件,因此拖拽的其他载荷(例如 evt.dataTransfer.getData('text/plain'))依然触手可及。只有当至少有一个文件被拒绝时才会调用 onRejected。

被拒绝项的 failedPropValidation 是 accept、max-file-size、max-total-size、max-files 或 filter 之一,指明该文件未能通过的那个选项(duplicate 也属于同一个 QRejectedEntry 类型,但只有 QFile 和 QUploader 在向列表追加文件时才会上报它)。

stopDropZone() 会释放该区域:元素不再接受投放(浏览器的默认处理行为重新生效),isOverDropZone 回到 false。该 composable 仍会持续遵循选项,因此把 target 指向另一个元素会重新激活它,而更改其他任何选项则不会。当你没有指定 target 时它尤其有用,因为组件自身的根元素无法被换掉(如果使用响应式的 target,把它设为 null 同样能释放该区域)。你无需在组件销毁时调用它,因为该 composable 会自行释放区域。

WARNING

文件被放到页面上任何其他位置时,会让浏览器导航到该文件(或下载它),从而离开你的应用。是否要防范这种情况由你决定:在 window 上监听 dragover 和 drop,并在这两个事件里都调用 preventDefault(),即可在任何地方取消这种导航。

运行时更改选项

选项可以是一个普通对象、一个 Ref 或一个 getter 函数。校验相关的选项和各个钩子会在每次投放时读取。使用 Ref 或 getter 时,该 composable 还会追踪 target 和 disabled 所读取的任何响应式状态,并在该状态变化时重新应用它们,因此你无需调用任何东西去「更新」它:

  • 切换 disabled 会释放该区域(isOverDropZone 回到 false,浏览器对投放的默认处理重新生效),随后再重新激活
  • 把 target 指向另一个元素(或让模板 ref 通过 v-if 发生变化)会跟随它
import { ref } from 'vue'
import { useDropZone } from 'quasar'

setup () {
  const uploading = ref(false)
  const allowVideos = ref(false)

  const { isOverDropZone, acceptedDropZoneFiles } = useDropZone(() => ({
    multiple: true,
    disabled: uploading.value,
    accept: allowVideos.value ? 'image/*,video/*' : 'image/*'
  }))

  // ...
}

示例

下面这个虚线框会接受拖入的图片,并在拖拽悬停其上时高亮。框内的按钮通过 useFilePicker 打开文件对话框,并使用相同的校验选项,两者共同填充同一个列表;被拒绝的文件会通过通知(notification)上报:

<template>
  <div class="q-pa-md q-gutter-md">
    <div
      ref="zone"
      class="drop-zone column flex-center q-pa-lg rounded-borders text-center"
      :class="isOverDropZone ? 'drop-zone--over' : ''"
    >
      <q-icon name="cloud_upload" size="48px" />
      <div class="q-mt-sm">Drop images here (up to 5MB each)</div>
      <q-btn
        push
        color="primary"
        label="Or pick them"
        class="q-mt-sm"
        @click="openFilePicker"
      />
    </div>

    <q-list
      v-if="files.length !== 0"
      bordered
      separator
      class="rounded-borders"
    >
      <q-item v-for="(file, index) in files" :key="index">
        <q-item-section>{{ file.name }}</q-item-section>
        <q-item-section side>{{
          format.humanStorageSize(file.size)
        }}</q-item-section>
      </q-item>
    </q-list>
  </div>
</template>

<script setup>
import { shallowRef, useTemplateRef } from 'vue'
import { format, useDropZone, useFilePicker, useQuasar } from 'quasar'

const $q = useQuasar()
const zone = useTemplateRef('zone')
const files = shallowRef([])

const options = {
  multiple: true,
  accept: 'image/*',
  maxFileSize: 5 * 1024 * 1024,
  onRejected(rejected) {
    $q.notify({
      type: 'negative',
      message: `${rejected.length} file(s) did not pass the validation`
    })
  }
}

function addFiles(accepted) {
  files.value = [...files.value, ...accepted]
}

const { isOverDropZone } = useDropZone({
  ...options,
  target: zone,
  onDrop: addFiles
})

const { openFilePicker } = useFilePicker({
  ...options,
  onChange: addFiles
})
</script>

<style lang="sass" scoped>
.drop-zone
  border: 2px dashed $grey-5
  transition: background-color .2s, border-color .2s

  &--over
    border-color: $primary
    background-color: rgba($primary, .08)
</style>

要预览拖入的图片(或者把任意拖入的文件交给 <img>、<video> 或下载链接),可以搭配 useObjectUrl composable,它会为 File 创建对象 URL,并在不再需要时将其释放。