unnamed-ui
输入控件

Upload

Upload component for file selection and upload with comprehensive features

单文件上传

多文件上传

"use client";

import { Upload } from "@/components/composed/upload/upload";

export function UploadDemo() {
  const handleSelect = (files: File[]) => {
    console.log("Selected files:", files);
  };

  const handleChange = (fileList: any[]) => {
    console.log("File list changed:", fileList);
  };

  return (
    <div className="w-full max-w-md space-y-6">
      <div>
        <h3 className="mb-2 text-sm font-medium">单文件上传</h3>
        <Upload
          buttonText="选择文件"
          onSelect={handleSelect}
          onChange={handleChange}
        />
      </div>

      <div>
        <h3 className="mb-2 text-sm font-medium">多文件上传</h3>
        <Upload
          multiple
          buttonText="选择多个文件"
          onSelect={handleSelect}
          onChange={handleChange}
        />
      </div>
    </div>
  );
}

Upload 组件用于文件选择和上传,支持单文件/多文件上传、文件类型限制、文件大小限制等功能,适用于头像上传、文档上传、图片上传等场景。

概述

  • 单文件/多文件上传:支持单文件和多文件上传模式
  • 文件限制:支持文件类型限制(accept)和文件大小限制(maxSize)
  • 自定义上传:支持自定义上传请求逻辑
  • 状态管理:完整的上传状态管理(idle、uploading、success、error)
  • 受控/非受控:支持受控和非受控两种使用模式
  • 类型安全:完整的 TypeScript 类型定义

快速开始

import { Upload } from "@/registry/wuhan/composed/upload/upload";

export function Example() {
  return <Upload buttonText="上传文件" />;
}

特性

  • 灵活的上传模式:支持单文件和多文件上传
  • 文件验证:内置文件类型和大小验证
  • 实时状态反馈:显示上传进度和状态(上传中、成功、失败)
  • 自定义请求:完全自定义的上传请求逻辑
  • 文件列表管理:支持文件列表的增删改查
  • 无缝集成:使用 block-button 组件,保持界面一致性

安装

pnpm dlx shadcn@latest add "http://localhost:3000/r/wuhan/upload.json"

代码演示

基础用法

最基础的上传组件使用。

"use client";

import { Upload } from "@/components/composed/upload/upload";

export function UploadDefault() {
  return (
    <div className="w-full max-w-md">
      <Upload />
    </div>
  );
}

单文件/多文件上传

演示单文件和多文件上传的区别。

单文件上传

多文件上传

"use client";

import { Upload } from "@/components/composed/upload/upload";

export function UploadDemo() {
  const handleSelect = (files: File[]) => {
    console.log("Selected files:", files);
  };

  const handleChange = (fileList: any[]) => {
    console.log("File list changed:", fileList);
  };

  return (
    <div className="w-full max-w-md space-y-6">
      <div>
        <h3 className="mb-2 text-sm font-medium">单文件上传</h3>
        <Upload
          buttonText="选择文件"
          onSelect={handleSelect}
          onChange={handleChange}
        />
      </div>

      <div>
        <h3 className="mb-2 text-sm font-medium">多文件上传</h3>
        <Upload
          multiple
          buttonText="选择多个文件"
          onSelect={handleSelect}
          onChange={handleChange}
        />
      </div>
    </div>
  );
}

自定义上传请求

使用 customRequest 实现自定义上传逻辑。

"use client";

import {
  Upload,
  type UploadFile,
} from "@/components/composed/upload/upload";

export function UploadCustomRequest() {
  // 模拟上传请求
  const handleUpload = async (file: File): Promise<any> => {
    return new Promise((resolve, reject) => {
      // 模拟网络延迟
      setTimeout(() => {
        // 模拟随机成功/失败
        if (Math.random() > 0.3) {
          resolve({
            url: `https://example.com/files/${file.name}`,
            id: Math.random().toString(36).substr(2, 9),
          });
        } else {
          reject(new Error("网络错误,上传失败"));
        }
      }, 2000);
    });
  };

  const handleChange = (fileList: UploadFile[]) => {
    console.log("Current file list:", fileList);
  };

  return (
    <div className="w-full max-w-md">
      <Upload
        multiple
        buttonText="上传到服务器"
        customRequest={handleUpload}
        onChange={handleChange}
      />
    </div>
  );
}

文件限制

演示文件类型和大小限制。

只允许上传图片

accept="image/*"

文件大小限制(2MB)

maxSize=2MB

最多上传 3 个文件

maxCount=3

只允许上传 PDF 和 Word 文档

accept=".pdf,.doc,.docx"

"use client";

import { Upload } from "@/components/composed/upload/upload";

export function UploadRestrictions() {
  return (
    <div className="w-full max-w-md space-y-6">
      <div>
        <h3 className="mb-2 text-sm font-medium">只允许上传图片</h3>
        <p className="mb-3 text-xs text-[var(--Text-text-tertiary)]">
          {`accept="image/*"`}
        </p>
        <Upload accept="image/*" buttonText="选择图片" />
      </div>

      <div>
        <h3 className="mb-2 text-sm font-medium">文件大小限制(2MB)</h3>
        <p className="mb-3 text-xs text-[var(--Text-text-tertiary)]">
          maxSize=2MB
        </p>
        <Upload
          multiple
          maxSize={2 * 1024 * 1024}
          buttonText="选择文件(≤2MB)"
        />
      </div>

      <div>
        <h3 className="mb-2 text-sm font-medium">最多上传 3 个文件</h3>
        <p className="mb-3 text-xs text-[var(--Text-text-tertiary)]">
          maxCount=3
        </p>
        <Upload multiple maxCount={3} buttonText="选择文件(最多3个)" />
      </div>

      <div>
        <h3 className="mb-2 text-sm font-medium">
          只允许上传 PDF 和 Word 文档
        </h3>
        <p className="mb-3 text-xs text-[var(--Text-text-tertiary)]">
          {`accept=".pdf,.doc,.docx"`}
        </p>
        <Upload accept=".pdf,.doc,.docx" buttonText="选择文档" />
      </div>
    </div>
  );
}

受控模式

使用受控模式管理文件列表。

已选择 0 个文件
"use client";

import * as React from "react";
import {
  Upload,
  type UploadFile,
} from "@/components/composed/upload/upload";

export function UploadControlled() {
  const [fileList, setFileList] = React.useState<UploadFile[]>([]);

  const handleChange = (newFileList: UploadFile[]) => {
    setFileList(newFileList);
  };

  const handleRemove = (file: UploadFile) => {
    console.log("Removing file:", file.name);
  };

  const handleClear = () => {
    setFileList([]);
  };

  return (
    <div className="w-full max-w-md space-y-4">
      <Upload
        fileList={fileList}
        multiple
        onChange={handleChange}
        onRemove={handleRemove}
      />

      <div className="flex items-center gap-4 text-sm">
        <span className="text-[var(--Text-text-tertiary)]">
          已选择 {fileList.length} 个文件
        </span>
        {fileList.length > 0 && (
          <button
            onClick={handleClear}
            className="text-[var(--Text-text-brand)] hover:underline"
          >
            清空列表
          </button>
        )}
      </div>
    </div>
  );
}

禁用状态

禁用上传功能。

"use client";

import { Upload } from "@/components/composed/upload/upload";

export function UploadDisabled() {
  return (
    <div className="w-full max-w-md">
      <Upload disabled buttonText="上传已禁用" />
    </div>
  );
}

API

Upload

上传组件的主要 API。

属性类型默认值说明
fileListUploadFile[]-文件列表(受控模式)
defaultFileListUploadFile[][]默认文件列表(非受控模式)
multiplebooleanfalse是否支持多文件上传
acceptstring-接受的文件类型,例如:"image/*" 或 ".jpg,.png"
maxSizenumber-文件大小限制(字节),例如:5 * 1024 * 1024 (5MB)
maxCountnumber-最大文件数量
disabledbooleanfalse是否禁用
buttonTextstring"上传文件"上传按钮文本
onSelect(files: File[]) => void-文件选择时的回调
onChange(fileList: UploadFile[]) => void-文件列表变化时的回调
onRemove(file: UploadFile) => void-删除文件时的回调
customRequest(file: File) => Promise<any>-自定义上传请求
classNamestring-自定义类名
itemRender(file: UploadFile, defaultRender: ReactNode) => ReactNode-自定义文件列表项渲染

UploadFile

文件对象类型定义。

interface UploadFile {
  uid: string;           // 文件唯一标识
  file: File;            // 原生文件对象
  name: string;          // 文件名
  size: number;          // 文件大小(字节)
  type: string;          // 文件类型
  status: UploadStatus;  // 上传状态
  progress?: number;     // 上传进度(0-100)
  error?: string;        // 错误信息
  response?: any;        // 响应数据
}

UploadStatus

上传状态类型。

type UploadStatus = "idle" | "uploading" | "success" | "error";

使用场景

1. 图片上传

<Upload
  accept="image/*"
  maxSize={5 * 1024 * 1024}
  buttonText="上传图片"
/>

2. 文档上传

<Upload
  accept=".pdf,.doc,.docx"
  multiple
  maxCount={5}
  buttonText="上传文档"
/>

3. 头像上传

<Upload
  accept="image/jpeg,image/png"
  maxSize={2 * 1024 * 1024}
  buttonText="上传头像"
  customRequest={async (file) => {
    const formData = new FormData();
    formData.append('avatar', file);
    const response = await fetch('/api/upload-avatar', {
      method: 'POST',
      body: formData,
    });
    return response.json();
  }}
/>

4. 批量文件上传

<Upload
  multiple
  maxCount={10}
  buttonText="批量上传"
  customRequest={uploadToServer}
  onChange={(fileList) => {
    console.log('当前文件列表:', fileList);
  }}
/>

设计指南

文件状态指示

  • 空闲(idle):文件已选择但未开始上传,显示普通文件图标
  • 上传中(uploading):文件正在上传,显示加载动画图标
  • 成功(success):文件上传成功,显示普通文件图标
  • 失败(error):文件上传失败,显示红色文件图标和错误信息

文件信息显示

每个文件项显示以下信息:

  • 文件状态图标(左侧)
  • 文件名(中间,支持截断)
  • 文件大小(文件名右侧)
  • 删除按钮(最右侧)

错误处理

  • 文件大小超限:立即显示错误状态,不触发上传
  • 上传失败:显示错误状态和错误信息
  • 类型不匹配:浏览器原生限制,无法选择

可访问性

  • 使用 aria-label 为删除按钮提供无障碍标签
  • 按钮支持键盘操作
  • 文件选择支持原生文件选择器
  • 状态变化有明确的视觉反馈

常见问题

如何实现自定义上传请求?

使用 customRequest 属性传入自定义上传函数:

const handleUpload = async (file: File) => {
  const formData = new FormData();
  formData.append('file', file);
  
  const response = await fetch('/api/upload', {
    method: 'POST',
    body: formData,
  });
  
  if (!response.ok) {
    throw new Error('上传失败');
  }
  
  return response.json();
};

<Upload customRequest={handleUpload} />

如何限制文件类型?

使用 accept 属性指定允许的文件类型:

// 只允许图片
<Upload accept="image/*" />

// 只允许特定格式
<Upload accept=".pdf,.doc,.docx" />

// 允许特定 MIME 类型
<Upload accept="image/jpeg,image/png" />

如何监听文件列表变化?

使用 onChange 回调监听文件列表变化:

<Upload
  onChange={(fileList) => {
    console.log('当前文件列表:', fileList);
    // 可以在这里处理文件列表变化
  }}
/>

如何使用受控模式?

传入 fileListonChange 实现受控模式:

const [fileList, setFileList] = useState<UploadFile[]>([]);

<Upload
  fileList={fileList}
  onChange={setFileList}
/>

原语组件

Upload 组件基于以下原语组件构建:

  • UploadContainerPrimitive: 上传容器
  • UploadTriggerPrimitive: 上传触发器
  • UploadInputPrimitive: 文件输入框
  • UploadFileListPrimitive: 文件列表容器
  • UploadFileItemPrimitive: 文件列表项
  • UploadFileIconPrimitive: 文件图标容器
  • UploadFileNamePrimitive: 文件名
  • UploadFileSizePrimitive: 文件大小
  • UploadFileRemovePrimitive: 删除按钮
  • UploadFileErrorPrimitive: 错误信息

这些原语组件可以单独使用来构建自定义的上传界面。