FileUpload 文件上传

点击或者拖放选择文件。

import { FileUpload } from '@hina-ui/react'

export default function Demo() {
  return (
    <FileUpload
      defaultValue={[]}
      multiple
      accept="image/*"
      aria-label="上传截图"
      className="w-96"
    />
  )
}
tsx

用法

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

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

值:空

'use client'

import { useState } from 'react'
import { FileUpload, Stack, Text } from '@hina-ui/react'

export default function Demo() {
  const [file, setFile] = useState<File | null>(null)

  return (
    <Stack gap="sm" align="stretch" className="w-96">
      <FileUpload
        value={file}
        onValueChange={next => setFile(next as File | null)}
        aria-label="上传封面"
      />
      <Text tone="muted" size="sm">
        值:{file?.name ?? '空'}
      </Text>
    </Stack>
  )
}
tsx

示例

多选

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

已选 0 个文件

'use client'

import { useState } from 'react'
import { FileUpload, Stack, Text } from '@hina-ui/react'

export default function Demo() {
  const [files, setFiles] = useState<File[]>([])

  return (
    <Stack gap="sm" align="stretch" className="w-96">
      <FileUpload
        value={files}
        onValueChange={next => setFiles(next as File[])}
        multiple
        aria-label="上传附件"
      />
      <Text tone="muted" size="sm">
        已选 {files.length} 个文件
      </Text>
    </Stack>
  )
}
tsx

类型、大小与数量限制

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

'use client'

import { useState } from 'react'
import { FileUpload, Stack, Text, type FileUploadRejection } from '@hina-ui/react'

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

export default function Demo() {
  const [notice, setNotice] = useState('')

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

  return (
    <Stack gap="sm" align="stretch" className="w-96">
      <FileUpload
        defaultValue={[]}
        multiple
        accept="image/*"
        maxSize={2 * 1024 * 1024}
        maxFiles={3}
        aria-label="上传图片"
        onReject={onReject}
      >
        最多 3 张图片,每张不超过 2 MB
      </FileUpload>
      {notice && (
        <Text tone="danger" size="sm">
          {notice}
        </Text>
      )}
    </Stack>
  )
}
tsx

按钮形态

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

import { FileUpload } from '@hina-ui/react'

export default function Demo() {
  return <FileUpload variant="button" accept=".pdf" aria-label="上传合同" className="w-96" />
}
tsx

即选即传

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

已交给上传流程:无

'use client'

import { useState } from 'react'
import { FileUpload, Stack, Text } from '@hina-ui/react'

export default function Demo() {
  const [files, setFiles] = useState<File[]>([])
  const [sent, setSent] = useState<string[]>([])

  function send(next: File | File[] | null) {
    const picked = Array.isArray(next) ? next : next ? [next] : []
    setSent(current => [...current, ...picked.map(file => file.name)])
    setFiles([])
  }

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

状态

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

import { ImagePlus } from 'lucide-react'
import { FileUpload, Stack } from '@hina-ui/react'

export default function Demo() {
  return (
    <Stack align="stretch" className="w-96">
      <FileUpload invalid aria-label="校验未通过" />
      <FileUpload disabled aria-label="已禁用" />
      <FileUpload loading aria-label="上传中">
        上传中
      </FileUpload>
      <FileUpload aria-label="上传图片" className="aspect-square w-40" icon={<ImagePlus />}>
        添加图片
      </FileUpload>
    </Stack>
  )
}
tsx

在表单中

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

图片,不超过 2MB

'use client'

import { useState } from 'react'
import * as v from 'valibot'
import { Button, FileUpload, Form, FormField, Text } from '@hina-ui/react'

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

export default function Demo() {
  const [values, setValues] = useState({ cover: null as File | null })
  const [saved, setSaved] = useState('')

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

  return (
    <Form values={values} rules={schema} className="w-80" onSubmit={save}>
      {({ submitting }) => (
        <>
          <FormField name="cover" label="封面" description="图片,不超过 2MB" required>
            <FileUpload
              value={values.cover}
              onValueChange={cover => setValues({ ...values, cover: cover as File | null })}
              accept="image/*"
            />
          </FormField>
          <Button type="submit" loading={submitting} className="self-start">
            上传
          </Button>
          {saved && (
            <Text tone="muted" size="sm">
              已上传:{saved}
            </Text>
          )}
        </>
      )}
    </Form>
  )
}
tsx

行为

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

无障碍

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

API

Props

属性
类型
默认值
说明
value
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
是否处于校验未通过状态
className
string
—
追加至根元素的类名

内容属性

属性
说明
children
拖放区或者按钮里的文字
icon
拖放区或者按钮里的图标

回调

回调
参数
说明
onValueChange
value: File | File[] | null
选中的文件变化
onReject
rejections: FileUploadRejection[]
有文件被拒绝,每项带 file 与 reason

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