FileUpload 文件上传

点击或者拖放选择文件。

<script setup lang="ts">
  import { ref } from 'vue'
  import { FileUpload } from '@hina-ui/vue'

  const files = ref<File[]>([])
</script>

<template>
  <FileUpload v-model="files" multiple accept="image/*" aria-label="上传截图" class="w-96" />
</template>
vue

用法

import { FileUpload } from '@hina-ui/vue'
ts

文件上传由一块拖放区与已选文件的列表组成:点击拖放区打开系统的文件选择框,把文件拖到区内放下同样可以选择。v-model 绑定选中的文件,单选时是一个 File 或者 null,multiple 时是 File 数组。组件只负责选择与展示,不发起上传,上传由调用方在拿到文件后自行处理。未声明的属性都会传给拖放区,请用 aria-label 或者 aria-labelledby 命名。

值:空

<script setup lang="ts">
  import { ref } from 'vue'
  import { FileUpload, Stack, Text } from '@hina-ui/vue'

  const file = ref<File | null>(null)
</script>

<template>
  <Stack gap="sm" align="stretch" class="w-96">
    <FileUpload v-model="file" aria-label="上传封面" />
    <Text tone="muted" size="sm">值:{{ file?.name ?? '空' }}</Text>
  </Stack>
</template>
vue

示例

多选

multiple 允许一次选择多个文件,再次选择时追加到列表,重复的文件只保留一份。

已选 0 个文件

<script setup lang="ts">
  import { ref } from 'vue'
  import { FileUpload, Stack, Text } from '@hina-ui/vue'

  const files = ref<File[]>([])
</script>

<template>
  <Stack gap="sm" align="stretch" class="w-96">
    <FileUpload v-model="files" multiple aria-label="上传附件" />
    <Text tone="muted" size="sm">已选 {{ files.length }} 个文件</Text>
  </Stack>
</template>
vue

类型、大小与数量限制

accept 与原生的同名属性一致,可以写后缀或者 MIME 类型;maxSize 以字节限制单个文件的大小;maxFiles 限制多选时的文件数量。不符合的文件不会进入列表,而是通过 reject 事件交出,参数带有每个文件被拒绝的原因。

<script setup lang="ts">
  import { ref } from 'vue'
  import { FileUpload, Stack, Text, type FileUploadRejection } from '@hina-ui/vue'

  const files = ref<File[]>([])
  const notice = ref('')

  const reasons: Record<FileUploadRejection['reason'], string> = {
    type: '不是图片',
    size: '超过 2 MB',
    count: '超过 3 个',
  }

  function onReject(rejections: FileUploadRejection[]) {
    notice.value = rejections.map(r => `${r.file.name}:${reasons[r.reason]}`).join(';')
  }
</script>

<template>
  <Stack gap="sm" align="stretch" class="w-96">
    <FileUpload
      v-model="files"
      multiple
      accept="image/*"
      :max-size="2 * 1024 * 1024"
      :max-files="3"
      aria-label="上传图片"
      @reject="onReject"
    >
      最多 3 张图片,每张不超过 2 MB
    </FileUpload>
    <Text v-if="notice" tone="danger" size="sm">{{ notice }}</Text>
  </Stack>
</template>
vue

按钮形态

variant="button" 把拖放区换成一颗按钮,适合放在表单行里。

<script setup lang="ts">
  import { ref } from 'vue'
  import { FileUpload } from '@hina-ui/vue'

  const file = ref<File | null>(null)
</script>

<template>
  <FileUpload v-model="file" variant="button" accept=".pdf" aria-label="上传合同" class="w-96" />
</template>
vue

即选即传

有些场景拿到文件就交给上传流程,结果由别处的界面呈现,例如图片库里的上传格。这时把 list 设为 false 只保留拖放区,在值变化时取走文件并清空。

已交给上传流程:无

<script setup lang="ts">
  import { ref } from 'vue'
  import { FileUpload, Stack, Text } from '@hina-ui/vue'

  const files = ref<File[]>([])
  const sent = ref<string[]>([])

  function send(next: File | File[] | null) {
    const picked = Array.isArray(next) ? next : next ? [next] : []
    sent.value.push(...picked.map(file => file.name))
    files.value = []
  }
</script>

<template>
  <Stack gap="sm" align="stretch" class="w-96">
    <FileUpload
      :model-value="files"
      :list="false"
      multiple
      accept="image/*"
      aria-label="上传图片"
      @update:model-value="send"
    >
      拖拽或者点击,选中即上传
    </FileUpload>
    <Text tone="muted" size="sm">已交给上传流程:{{ sent.length ? sent.join('、') : '无' }}</Text>
  </Stack>
</template>
vue

状态

invalid 给拖放区加上警示色,disabled 禁用选择与移除,loading 在上传期间把图标换成加载指示并禁用选择。icon 插槽替换拖放区或者按钮里的图标;根元素被撑成固定尺寸时,拖放区随之填满。

<script setup lang="ts">
  import { ImagePlus } from '@lucide/vue'
  import { FileUpload, Stack } from '@hina-ui/vue'
</script>

<template>
  <Stack align="stretch" class="w-96">
    <FileUpload invalid aria-label="校验未通过" />
    <FileUpload disabled aria-label="已禁用" />
    <FileUpload loading aria-label="上传中">上传中</FileUpload>
    <FileUpload aria-label="上传图片" class="aspect-square w-40">
      <template #icon>
        <ImagePlus />
      </template>
      添加图片
    </FileUpload>
  </Stack>
</template>
vue

在表单中

放进 FormField 后,标签指向拖放区,说明与错误信息由字段渲染;校验规则与提交交给 Form。值是 File 对象,大小、类型这类规则可以直接写在校验里。

图片,不超过 2MB

<script setup lang="ts">
  import { reactive, ref } from 'vue'
  import * as v from 'valibot'
  import { Button, FileUpload, Form, FormField, Text } from '@hina-ui/vue'

  const schema = v.object({
    cover: v.pipe(
      v.instance(File, '请选择封面图片'),
      v.check(file => file.size <= 2 * 1024 * 1024, '图片不能超过 2MB'),
    ),
  })

  const values = reactive({ cover: null as File | null })
  const saved = ref('')

  async function save(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 600))
    saved.value = (data as { cover: File }).cover.name
  }
</script>

<template>
  <Form v-slot="{ submitting }" :values="values" :rules="schema" class="w-80" @submit="save">
    <FormField name="cover" label="封面" description="图片,不超过 2MB" required>
      <FileUpload v-model="values.cover" accept="image/*" />
    </FormField>
    <Button type="submit" :loading="submitting" class="self-start">上传</Button>
    <Text v-if="saved" tone="muted" size="sm">已上传:{{ saved }}</Text>
  </Form>
</template>
vue

行为

  • 点击拖放区或者按钮打开系统的文件选择框;把文件拖到拖放区上时边框换成强调色,放下即选择。
  • 单选时新选择的文件替换原来的;多选时追加,超过 maxFiles 的部分被拒绝。
  • 列表里每个文件显示名称与可读的大小,图片显示缩略图;点击移除按钮从列表中删掉该文件。
  • 默认插槽替换拖放区或者按钮里的文字。

无障碍

  • 拖放区是一枚按钮,可以用键盘触达,Enter 或者空格打开文件选择框;隐藏的文件输入对辅助技术不可见。
  • 每个文件的移除按钮带有语言包给出的名称,包含文件名。
  • 通过 aria-label 或者 aria-labelledby 为拖放区命名。

API

Props

属性
类型
默认值
说明
modelValue
File | File[] | null
null
选中的文件,multiple 时是数组
multiple
boolean
false
是否允许多选
accept
string
—
接受的类型,与原生属性一致
maxSize
number
—
单个文件的大小上限,单位字节
maxFiles
number
—
多选时的数量上限
variant
'area' | 'button'
'area'
形态
list
boolean
true
是否列出选中的文件
preview
boolean
true
是否为图片显示缩略图
name
string
—
表单字段名
loading
boolean
false
是否显示加载指示并禁用选择
disabled
boolean
false
是否禁用
invalid
boolean
false
是否处于校验未通过状态
class
string
—
追加至根元素的类名

插槽

插槽
说明
default
拖放区或者按钮里的文字
icon
拖放区或者按钮里的图标

事件

事件
参数
说明
update:modelValue
value: File | File[] | null
选中的文件变化
reject
rejections: FileUploadRejection[]
有文件被拒绝,每项带 file 与 reason

FileUploadRejection 的 reason 是 'type'、'size' 或者 'count',类型可以从包入口导入。