# Upload

文件上传组件，支持单文件/多文件上传、进度展示、文件列表管理及拖拽上传。

## 适用场景

- 表单中的文件附件上传（带文件列表展示）
- 需要展示上传进度、错误状态和重试操作的场景
- 需要拖拽上传的场景
- 需要自定义文件列表渲染的场景

## Props

### Upload Props

| 名称 | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|------|------|
| className | `string` | `""` | 否 | 自定义 class |
| fileList | `UploadFileObject[]` | `-` | 否 | 文件列表控制值。设为 `null` 为"无 fileList 情景" |
| defaultFileList | `UploadFileObject[]` | `-` | 否 | 文件列表默认值，不支持设为 `null` |
| disabled | `boolean` | `-` | 否 | 是否禁用 |
| url | `string` | `-` | 否 | 上传地址 |
| method | `string` | `"POST"` | 否 | 请求方法 |
| multiple | `boolean` | `true` | 否 | 是否允许多选 |
| accept | `string \| string[]` | `-` | 否 | 接受上传的文件类型 |
| maxSize | `number` | `-` | 否 | 最大文件大小（bytes） |
| maxLength | `number` | `-` | 否 | 最大文件总数量（多次上传累计） |
| params | `StringIndexedObject<string \| number>` | `-` | 否 | 请求参数 |
| headers | `StringIndexedObject<string>` | `-` | 否 | 请求头 |
| fileFieldName | `string` | `"file"` | 否 | 上传参数中文件字段的 name 值 |
| hint | `ReactNode` | `-` | 否 | 提示信息 |
| allowDuplicate | `boolean` | `false` | 否 | 是否允许重复文件名，重名文件会标记序号前缀 `(n)` |
| renderFileList | `(renderProps: FileListRenderProps) => ReactNode` | `-` | 否 | 自定义文件列表渲染。指定为 `() => null` 可分离文件列表与上传按钮的渲染 |
| isResponseSuccess | `(respJson: Json, respText: string) => boolean` | `-` | 否 | 判断上传请求是否成功的函数 |
| getResponseMessage | `(respJson: Json, respText: string) => string` | `-` | 否 | 获取响应消息的函数 |
| onChange | `(fileList: UploadFileObject[]) => void` | `-` | 否 | 文件列表变化回调，任一文件状态变化时触发 |
| onRemove | `(file, fileList, currentFileList) => boolean \| void \| Promise<any>` | `-` | 否 | 删除文件的回调。返回 `false` 阻止删除，返回 Promise 则 resolve 后删除 |
| onLoadStart | `(file: UploadFileObject, fileList: UploadFileObject[]) => void` | `-` | 否 | 文件上传开始的回调（progress === 0） |
| onLoadProgress | `(file: UploadFileObject, fileList: UploadFileObject[]) => void` | `-` | 否 | 文件上传进度变化的回调 |
| onLoadSuccess | `(file: UploadFileObject, fileList: UploadFileObject[]) => void` | `-` | 否 | 文件上传成功的回调 |
| onLoadFail | `(file: UploadFileObject, fileList: UploadFileObject[]) => void` | `-` | 否 | 文件上传失败的回调 |
| onDropAccepted | `(files: File[], e: DropEvent) => File[]` | `-` | 否 | UploadDropzone 接受文件时的回调，返回值将替代原 files 处理 |
| onDropRejected | `(files: File[], e: DropEvent) => File[]` | `-` | 否 | UploadDropzone 拒绝文件时的回调 |
| size | `"md" \| "sm" \| "xs" \| "lg"` | `-` | 否 | 尺寸，默认使用 config context 的 `baseSize` |
| layout | `"horizontal" \| "vertical"` | `-` | 否 | 文件列表排列布局，`vertical` 每行一个，`horizontal` 横向排列 |
| onPreview | `FileActionHandler` | `-` | 否 | 点击预览回调 |
| onDownload | `FileActionHandler` | `-` | 否 | 点击下载回调（仅在无 `downloadUrl` 时生效） |
| downloadMethod | `DownloadIframeMethod` | `-` | 否 | 下载请求方法，仅 `downloadType="download-button"` 时有效 |
| downloadType | `"button" \| "download-button"` | `-` | 否 | 下载方式。`download-button` 使用 Download 组件，`button` 使用普通按钮 |
| downloadTarget | `string` | `-` | 否 | `<a />` 下载时的 target，仅 `downloadType="button"` 时有效 |

### UploadFileObject Props

| 名称 | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|------|------|
| name | `string` | `-` | 是 | 文件名 |
| id | `string \| number` | `-` | 否 | 唯一标识 |
| file | `File` | `-` | 否 | 原始 File 对象 |
| xhr | `XMLHttpRequest` | `-` | 否 | 文件上传的 xhr 对象 |
| responseText | `string` | `-` | 否 | 上传响应结果 text |
| responseJson | `Json` | `-` | 否 | 上传响应结果 json（仅 responseText 为合法 json 时有值） |
| className | `string` | `-` | 否 | 自定义 class |
| type | `"img" \| "pdf" \| "excel" \| "ppt" \| "word" \| "zip" \| "eml" \| "html" \| "txt" \| "other"` | `-` | 否 | 文件类型 |
| previewUrl | `string` | `-` | 否 | 预览 url |
| downloadUrl | `string` | `-` | 否 | 下载 url |
| downloadTarget | `string` | `-` | 否 | 下载 anchor 的 target |
| error | `string \| boolean` | `-` | 否 | 错误信息或是否处于错误状态 |
| progress | `number \| boolean` | `-` | 否 | 上传进度或是否处于上传中状态 |
| disableRemove | `boolean` | `-` | 否 | 是否禁止删除 |
| disableRetry | `boolean` | `-` | 否 | 是否禁止重试 |
| disableDownload | `boolean` | `-` | 否 | 是否禁止下载 |
| disablePreview | `boolean` | `-` | 否 | 是否禁止预览 |
| size | `number` | `-` | 否 | 文件大小（bytes） |

### FileListRenderProps

> `renderFileList` 回调的参数类型。

| 名称 | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|------|------|
| fileList | `UploadFileObject[]` | `-` | 是 | 当前文件列表 |
| remove | `(fileObj: UploadFileObject) => void` | `-` | 是 | 删除文件，未完成上传时同时 abort xhr |
| retry | `(fileObj: UploadFileObject) => void` | `-` | 是 | 重试上传 |

## 典型用法

### 基础用法

```tsx
import { useState } from 'react'
import { Upload, UploadFileObject } from '@befe/brick'

const [fileList, setFileList] = useState<UploadFileObject[]>([
    { name: 'logo.png', previewUrl: '...', downloadUrl: '...' },
    { name: 'logo-protect.png', previewUrl: '...', disableRemove: true, disableDownload: true },
    { name: 'report.xls', error: '上传失败：xxxxxx' },
])

<Upload
    url="/api/upload"
    headers={{ token: 'some-token' }}
    maxSize={3 * 1024 * 1024}
    accept="image/png"
    params={{ parentId: '1234' }}
    multiple
    fileList={fileList}
    onChange={setFileList}
    hint="1、单个附件大小<=3M；2、只接受 .png 格式"
/>
```

### 限制文件数量

```tsx
<Upload
    url="/api/upload"
    maxLength={2}
    multiple
    hint="附件个数<=2个"
    layout="horizontal"
/>
```

### 自定义文件列表渲染

> `renderFileList` 提供自定义渲染"文件列表"的能力；指定为 `() => null` 可将文件列表与上传按钮分开渲染。

```tsx
<Upload
    url="/api/upload"
    fileList={fileList}
    onChange={setFileList}
    renderFileList={({ fileList, remove }) => (
        <ul className="custom-file-list">
            {fileList.map((file, idx) => (
                <li key={file.name}>{`${idx}. ${file.name}`}</li>
            ))}
        </ul>
    )}
/>
```

### 无文件列表模式

> `fileList: null` 与 `renderFileList: () => null` **并不等效**：前者是"无 fileList 语义"（不处置 fileList、不做重名判断、回调 fileList 持续累加）；后者仍是"列表语义"（含重名判断），只是 UI 由你自行渲染。
> 无 fileList 模式下没有内置列表展示失败态，失败信息只能从 `onLoadFail(file)` 拿，自行 toast / message。`getResponseMessage` 只作用于内置 FileList，此处不参与；服务端文案需自取 `file.responseJson` / `file.responseText`。

```tsx
import { toast } from '@befe/brick'

<Upload
    url="/api/upload"
    fileList={null}
    onLoadSuccess={file => console.log('上传成功', file)}
    onLoadFail={file => {
        const respJson = file.responseJson as { message?: string } | undefined
        let msg = respJson?.message
        if (!msg && file.responseText) {
            try {
                msg = (JSON.parse(file.responseText) as { message?: string })?.message
            } catch {
                // responseText 非 JSON（HTML/空），保持兜底文案
            }
        }
        toast.error(msg || '上传失败')
    }}
/>
```

## 注意事项

- `maxLength` 是对**多次上传总数量**的限制，而非单次选择的数量限制
- `fileList: null`（无 fileList 模式）与 `renderFileList: () => null` 语义不同：前者不处置 fileList、不做重名判断（`allowDuplicate` 被忽略）、回调 fileList 持续累加；后者仍是列表语义，只是自定义渲染 UI
- 无 fileList 模式下失败文案只能从 `onLoadFail(file)` 取（`file.responseJson` / `file.responseText`）；`getResponseMessage` 仅作用于内置 FileList，此时不参与。要文案准确需后端失败时返回结构化 JSON 错误
- `onRemove` 返回 `false` 阻止删除；返回 Promise 时等待 resolve 后删除，reject 则不删除
- 有 `downloadUrl` 的文件项使用 Download 组件下载，不绑定 `onDownload` 回调
- 上传成功后需在 `onChange` 回调中将服务端返回的 `previewUrl`、`downloadUrl` 等信息合并回 fileList
