Skip to page content

v-drop-zone 指令
v2.34+

“DropZone” 是一个 Quasar 指令,它会把所应用的 DOM 元素(或组件)变成一个目标区域,用来接收从桌面拖入的文件:拖放发生时会调用你指定的方法并传入被拖入的 File 对象,同时在拖拽悬停期间给该元素打上一个 CSS 类作为标记。

它是 useDropZone composable 的模板端形式。它的对象写法会像 QFile 那样校验被拖入的文件(accept、大小与数量上限、filter);而 composable 则额外提供了响应式状态、进入/离开钩子以及可自由指定的目标元素,所以当你需要这些能力时就应该改用 composable。

用法

指令的值就是处理函数。它接收两个参数:被拖入的 File 对象数组,以及拖放事件(事件的 dataTransfer 里还携带着本次拖拽的其他载荷,比如文本或链接)。每次拖放都会调用该处理函数;如果这次拖拽没有携带任何文件,则传入一个空数组。

当有东西被拖拽到该元素上方(包括它的子元素)时,元素会带上 q-drop-zone--over 这个 CSS 类,默认会在元素内部绘制一圈虚线轮廓(与 QFile、QUploader 的反馈效果一致)。

TIP

仅凭一个拖放区域,键盘和触屏用户是无法访问它的。请在其内部(或旁边)放一个按钮,通过 QFile 或 useFilePicker composable 打开文件选择对话框。

基础用法

默认只会把拖入的第一个文件交给处理函数。加上 multiple 修饰符(例如 v-drop-zone.multiple)后,处理函数就能收到拖入的每一个文件:

<template>
  <div class="q-pa-md q-gutter-md">
    <div
      v-drop-zone.multiple="onDrop"
      class="drop-zone column flex-center q-pa-lg rounded-borders text-center"
    >
      <q-icon name="cloud_upload" size="48px" />
      <div class="q-mt-sm">Drop files here</div>
    </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 } from 'vue'
import { format } from 'quasar'

const files = shallowRef([])

function onDrop(dropped) {
  files.value = [...files.value, ...dropped]
}
</script>

<style lang="sass" scoped>
.drop-zone
  border: 1px solid $grey-5
</style>

自定义样式

默认的反馈是一圈 currentColor 色的虚线轮廓。如果想为应用中所有拖放区域统一修改它,可以在全局 CSS 中重新设置 .q-drop-zone--over 的样式。如果只想改某一个元素,则改用对象写法,通过 activeClass 选项传入你自己的类;此时默认的类(以及它的轮廓)就不会再被应用:

<template>
  <div class="q-pa-md">
    <div
      v-drop-zone="{
        handler: onDrop,
        multiple: true,
        activeClass: 'drop-zone--active'
      }"
      class="drop-zone column flex-center q-pa-lg rounded-borders text-center"
    >
      <q-icon name="add_photo_alternate" size="48px" />
      <div class="q-mt-sm">{{ count }} file(s) dropped so far</div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)

function onDrop(files) {
  count.value += files.length
}
</script>

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

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

校验

对象写法把处理函数与 QFile 的校验选项并列在一起:accept、maxFileSize、maxTotalSize、maxFiles(每次拖放的上限)和 filter,外加 multiple(它的优先级高于修饰符)。只有通过校验的文件才会被交给 handler;未通过的文件会以 { failedPropValidation, file } 条目的形式上报给 onRejected,与 QFile 触发 @rejected 的方式一致。这些选项在每次拖放时都会重新读取,因此可以直接写一个内联对象字面量,并且它可以在运行时动态变化:

<template>
  <div class="q-pa-md q-gutter-md">
    <div
      v-drop-zone="{
        handler: onDrop,
        multiple: true,
        accept: 'image/*',
        maxFileSize: 1024 * 1024,
        onRejected
      }"
      class="drop-zone column flex-center q-pa-lg rounded-borders text-center"
    >
      <q-icon name="add_photo_alternate" size="48px" />
      <div class="q-mt-sm">Drop images here (up to 1MB each)</div>
    </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 } from 'vue'
import { format, useQuasar } from 'quasar'

const $q = useQuasar()
const files = shallowRef([])

function onDrop(dropped) {
  files.value = [...files.value, ...dropped]
}

function onRejected(rejected) {
  $q.notify({
    type: 'negative',
    message: `${rejected.length} file(s) did not pass the validation`
  })
}
</script>

<style lang="sass" scoped>
.drop-zone
  border: 1px solid $grey-5
</style>

禁用

如果传入 false 或 null 而不是一个函数(其实任何非处理函数的值都可以,比如一个不含 handler 的对象),就会禁用该指令:元素会停止接收拖放(浏览器对拖放的默认处理会重新生效),直到再次提供处理函数为止。整个过程中 DOM 元素本身不受影响,所以它所包裹的内容会保持原有状态。

<template>
  <div class="q-pa-md q-gutter-md">
    <q-toggle v-model="enabled" label="Accept drops" />

    <div
      v-drop-zone="enabled ? onDrop : false"
      class="drop-zone column flex-center q-pa-lg rounded-borders text-center"
      :class="{ 'drop-zone--disabled': !enabled }"
    >
      <q-icon name="cloud_upload" size="48px" />
      <div class="q-mt-sm">
        {{ enabled ? 'Drop a file here' : 'Drops are not accepted' }}
      </div>
    </div>

    <div v-if="lastFile !== null">Last dropped file: {{ lastFile.name }}</div>
  </div>
</template>

<script setup>
import { ref, shallowRef } from 'vue'

const enabled = ref(true)
const lastFile = shallowRef(null)

function onDrop(files) {
  lastFile.value = files[0]
}
</script>

<style lang="sass" scoped>
.drop-zone
  border: 1px solid $grey-5
  transition: opacity .2s

  &--disabled
    opacity: .5
</style>
WARNING

如果文件被拖放到页面上其他任何地方,浏览器会导航到该文件(或将它下载下来),从而离开你的应用。是否要防范这种情况由你决定:在 window 上监听 dragover 和 drop 事件,并对二者都调用 preventDefault(),就能在整个页面范围内取消这种导航。