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
即选即传
有些场景拿到文件就交给上传流程,结果由别处的界面呈现,例如图片库里的上传格。这时把 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 对象,大小、类型这类规则可以直接写在校验里。
'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',类型可以从包入口导入。