React 文件上传新玩法:Aliyun OSS 加持的智能上传组件

news2025/5/12 7:53:54

文件上传是前端开发中的“老朋友”,但如何让它既简单又强大,还能无缝对接云端存储?今天,我要带你认识一个超酷的 React 组件 AliUploader,它不仅支持拖拽上传、批量编辑和文件排序,还直接把文件传到 Aliyun OSS(阿里云对象存储服务),返回云端链接供你随时调用。我们会拆解它的代码,优化它的逻辑,再通过一个“项目文件上传器” Demo 展示它的实力。准备好了吗?让我们一起把文件上传玩出云端新高度吧!


组件的核心功能:上传到云端

AliUploader 的任务是让文件管理变得简单又高效:

  1. OSS 直传:文件上传直接到 Aliyun OSS,返回云端链接。
  2. 拖拽上传:支持单文件或批量拖拽,操作丝滑。
  3. 文件管理:按类型分组(图片、文档、其他),支持排序和批量编辑备注。
  4. 云端同步:初始化时加载 OSS 文件列表,删除时同步清理云端。

它就像一个“云端快递员”,把你的文件快速送上云端,还能随时查收!

代码拆解与优化

原始代码的问题

原始代码已经很强大,但有几处可以改进:

  • 上传状态管理混乱:初始文件状态未明确定义,进度更新不完整。
  • 错误处理不足:上传和删除的异常捕获不够健壮。
  • 性能优化空间:文件列表更新可以更高效。

我们将优化这些点,让代码更优雅、更健壮。


优化后的代码

1. 类型定义与工具函数
// utils.ts
import OSS from 'ali-oss';
import { message } from 'antd';
import { FileData, Props } from './index';

export function getFileType(fileName: string): FileData['type'] {
  if (/\.(png|jpg|jpeg|gif)$/i.test(fileName)) return 'image';
  if (/\.(doc|docx|pdf|xls|xlsx|pptx)$/i.test(fileName)) return 'document';
  return 'other';
}

export async function uploadToOSS(
  file: File,
  ossConfig: Props['ossConfig'],
  onProgress: (data: Partial<FileData>) => void,
): Promise<FileData> {
  if (!ossConfig) throw new Error('OSS 配置未提供');
  const client = new OSS({
    region: ossConfig.region,
    accessKeyId: ossConfig.accessKeyId,
    accessKeySecret: ossConfig.accessKeySecret,
    bucket: ossConfig.bucket,
  });

  const fileName = `${Date.now()}-${file.name}`;
  const result = await client.put(fileName, file, {
    progress: p => onProgress({ percent: Math.round(p * 100) }),
  });
  if (result.res.status !== 200) throw new Error('OSS 上传失败');

  const url = result.url || `https://${ossConfig.bucket}.${ossConfig.region}.aliyuncs.com/${fileName}`;
  return {
    uid: `${Date.now()}-${Math.random()}`,
    fileId: fileName,
    name: file.name,
    thumbUrl: url,
    url,
    status: 'done',
    percent: 100,
    type: getFileType(file.name),
    uploadTime: Date.now(),
    cloudUrl: url,
  };
}

export async function delfileFromOSS(
  fileName: string,
  ossConfig: Props['ossConfig'],
): Promise<void> {
  const client = new OSS({ ...ossConfig! });
  const result = await client.delete(fileName);
  if (result.res.status !== 204) throw new Error('OSS 删除失败');
  message.success(`文件 ${fileName} 已从 OSS 删除`);
}

export async function getOSSList(ossConfig: Props['ossConfig']): Promise<FileData[]> {
  const client = new OSS({ ...ossConfig! });
  const result = await client.list();
  if (result.res.status !== 200) throw new Error('获取 OSS 文件列表失败');
  return result.objects.map((r: any) => ({
    uid: `${r.name}-${Date.now()}`,
    fileId: r.name,
    name: r.name,
    thumbUrl: r.url,
    url: r.url,
    status: 'done',
    percent: 100,
    type: getFileType(r.name),
    uploadTime: new Date(r.lastModified).getTime(),
  }));
}

export function validateFile(file: File, accept: string, maxBytes: number): boolean {
  const types = accept.split(',').map(t => t.trim());
  const isValidType = types.some(t => file.name.endsWith(t));
  const isValidSize = file.size / 1024 / 1024 < maxBytes;
  if (!isValidType) message.warning('上传文件格式不支持');
  if (!isValidSize) message.warning(`文件大小不能超过${maxBytes}MB`);
  return isValidType && isValidSize;
}

优化亮点

  • uploadToOSS:规范化返回 FileData,支持进度更新。
  • delfileFromOSS:移除回调,简化逻辑。
  • getOSSList:直接返回文件列表,优化数据处理。
2. 主组件:AliUploader
import React, { useState, useEffect } from 'react';
import { Upload, Button, Collapse, Select, Modal, Input, message } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import { FileData, Props } from './index';
import { FileItem } from './FileItem';
import { uploadToOSS, delfileFromOSS, getOSSList, validateFile } from './utils';
import './index.less';

const { Panel } = Collapse;
const { Option } = Select;

const AliUploader: React.FC<Props> = ({
  accept = '.doc,.docx,.xls,.xlsx,.pdf,.pptx,.png,.jpg',
  uploadName = '上传文件',
  listType = 'text',
  maxCount = 1,
  maxBytes = 20,
  multiple = false,
  fileList = [],
  ossConfig,
  showUploadList = true,
  disabled = false,
  extraTip,
  showTips = true,
  onChange,
  onLoading,
  onSuccess,
  filedIds,
}) => {
  const [uploadFileList, setUploadFileList] = useState<FileData[]>(fileList);
  const [loading, setLoading] = useState(false);
  const [sortBy, setSortBy] = useState<'time' | 'name'>('time');
  const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
  const [batchEditVisible, setBatchEditVisible] = useState(false);
  const [batchNote, setBatchNote] = useState('');

  const handleUpload = async (file: File, files?: File[]) => {
    const uploadFiles = multiple && files ? files : [file];
    if (uploadFileList.length + uploadFiles.length > maxCount) {
      message.warning(`最多上传${maxCount}个文件`);
      return;
    }

    setLoading(true);
    onLoading?.(true);
    const newFiles = uploadFiles.map(f => ({
      uid: `${Date.now()}-${Math.random()}`,
      name: f.name,
      thumbUrl: '',
      status: 'uploading' as const,
      percent: 0,
      type: getFileType(f.name),
      uploadTime: Date.now(),
    }));
    setUploadFileList(prev => (maxCount === 1 ? newFiles : [...prev, ...newFiles]));

    try {
      const results = await Promise.all(
        uploadFiles.map((f, i) =>
          uploadToOSS(f, ossConfig!, data =>
            setUploadFileList(prev =>
              prev.map(item => (item.uid === newFiles[i].uid ? { ...item, ...data } : item)),
            ),
          ),
        ),
      );
      const finalList = maxCount === 1 ? results : [...uploadFileList.filter(f => !newFiles.some(n => n.uid === f.uid)), ...results];
      setUploadFileList(finalList);
      onChange?.(finalList);
      onSuccess?.(finalList);
      filedIds?.(finalList.map(f => f.fileId));
      message.success(`${results.length}个文件上传成功`);
    } catch (error) {
      setUploadFileList(prev =>
        prev.map(f => (newFiles.some(n => n.uid === f.uid) ? { ...f, status: 'error' } : f)),
      );
      message.error('部分文件上传失败');
    } finally {
      setLoading(false);
      onLoading?.(false);
    }
  };

  const handleRemove = async (file: FileData) => {
    try {
      await delfileFromOSS(file.fileId, ossConfig!);
      const newList = uploadFileList.filter(f => f.uid !== file.uid);
      setUploadFileList(newList);
      setSelectedFiles(prev => prev.filter(uid => uid !== file.uid));
      onChange?.(newList);
      filedIds?.(newList.map(f => f.fileId));
    } catch (error) {
      message.error('删除失败');
    }
  };

  const handleEdit = (editedFile: FileData) => {
    const newList = uploadFileList.map(f => (f.uid === editedFile.uid ? editedFile : f));
    setUploadFileList(newList);
    onChange?.(newList);
  };

  const handleSelect = (uid: string, selected: boolean) => {
    setSelectedFiles(prev => (selected ? [...prev, uid] : prev.filter(id => id !== uid)));
  };

  const handleBatchEdit = () => {
    if (selectedFiles.length === 0) {
      message.warning('请先选择文件');
      return;
    }
    setBatchEditVisible(true);
  };

  const applyBatchEdit = () => {
    const newList = uploadFileList.map(f =>
      selectedFiles.includes(f.uid) ? { ...f, note: batchNote } : f,
    );
    setUploadFileList(newList);
    onChange?.(newList);
    setBatchEditVisible(false);
    setSelectedFiles([]);
    setBatchNote('');
  };

  useEffect(() => {
    if (ossConfig) {
      getOSSList(ossConfig)
        .then(files => setUploadFileList(prev => [...prev, ...files]))
        .catch(() => message.error('获取 OSS 文件列表失败'));
    }
  }, [ossConfig]);

  const groupedFiles = {
    image: uploadFileList.filter(f => f.type === 'image'),
    document: uploadFileList.filter(f => f.type === 'document'),
    other: uploadFileList.filter(f => f.type === 'other'),
  };

  const sortFiles = (files: FileData[]) =>
    files.sort((a, b) =>
      sortBy === 'time' ? b.uploadTime - a.uploadTime : a.name.localeCompare(b.name),
    );

  return (
    <div className="fileUpload">
      <Upload.Dragger
        accept={accept}
        listType={listType as any}
        maxCount={maxCount}
        multiple={multiple}
        beforeUpload={(file, fileList) => {
          if (validateFile(file, accept, maxBytes)) {
            handleUpload(file, fileList);
          }
          return false; // 阻止默认上传
        }}
        fileList={[]}
        disabled={disabled || loading}
      >
        <Button icon={<UploadOutlined />} loading={loading} disabled={disabled}>
          {uploadName}
        </Button>
        {showTips && (
          <div className="tip">
            {`支持${maxBytes}MB以内的${accept}文件(可拖拽上传,直接存至 OSS)`}
          </div>
        )}
      </Upload.Dragger>
      {showUploadList && (
        <div>
          <div style={{ margin: '10px 0' }}>
            <Select value={sortBy} onChange={setSortBy} style={{ width: 120, marginRight: 10 }}>
              <Option value="time">按时间排序</Option>
              <Option value="name">按名称排序</Option>
            </Select>
            <Button onClick={handleBatchEdit} disabled={selectedFiles.length === 0}>
              批量编辑 ({selectedFiles.length})
            </Button>
          </div>
          <Collapse defaultActiveKey={['image', 'document', 'other']}>
            {Object.entries(groupedFiles).map(([type, files]) =>
              files.length > 0 ? (
                <Panel
                  header={`${type === 'image' ? '图片' : type === 'document' ? '文档' : '其他'} (${files.length})`}
                  key={type}
                >
                  {sortFiles(files).map(file => (
                    <FileItem
                      key={file.uid}
                      file={file}
                      onRemove={() => handleRemove(file)}
                      onEdit={handleEdit}
                      onSelect={handleSelect}
                      selected={selectedFiles.includes(file.uid)}
                    />
                  ))}
                </Panel>
              ) : null,
            )}
          </Collapse>
        </div>
      )}
      {extraTip && <div className="extraTip">{extraTip}</div>}
      <Modal
        open={batchEditVisible}
        title={`批量编辑 (${selectedFiles.length} 个文件)`}
        onOk={applyBatchEdit}
        onCancel={() => setBatchEditVisible(false)}
      >
        <Input
          value={batchNote}
          onChange={e => setBatchNote(e.target.value)}
          placeholder="输入统一备注"
        />
      </Modal>
    </div>
  );
};

export default AliUploader;
.imageList {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 8px;
  padding: 8px;
  border: 1px solid #d9d9d9;
  border-radius: 2px;

  .deleteBtn {
    color: rgba(0, 0, 0, 0.45);
  }
}

.fileList {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 8px;
  padding: 8px;
  border: 1px solid #d9d9d9;
  border-radius: 2px;

  .deleteBtn {
    color: rgba(0, 0, 0, 0.45);
    visibility: hidden;
  }

  &:hover {
    background-color: #f5f5f5;

    .deleteBtn {
      visibility: visible;
    }
  }
}

.fileName {
  margin: 0 8px;
}

优化亮点

  1. 上传逻辑:规范化状态管理,修复初始状态问题。
  2. 错误处理:增加 try-catch,提升健壮性。
  3. 性能优化:避免重复添加文件,提升列表更新效率。

Demo:项目文件上传器(OSS 版)

我们用一个“项目文件上传器”展示 AliUploader 的功能。

使用示例

如何使用这个组件?

该组件已集成到 react-nexlif 开源库中。 具体文档可参考详情文档。你可以通过以下方式引入并使用:

pnpm install react-nexlif

import React, { useState, useRef } from 'react';
import { AliUploader } from 'react-nexlif';
import { ApartmentOutlined } from '@ant-design/icons';
// import {ossConfig} from './utils';

const App: React.FC = () => {
  const uploadRef = useRef(null);

  const ossConfig = {
  region: 'oss-cn-hangzhou',
  accessKeyId: '',
  accessKeySecret: '',
  bucket: '',
};

  const handleChange = (list: any[]) => {
    // console.log('当前文件列表:', list);
  };

  const handleSuccess = (list: any[]) => {
    // console.log('上传成功:', list);
    list.forEach((file) => {
      // console.log(`文件 ${file.name} 的 OSS 链接: ${file.url}`),
    });
  };

  const handleIds = (ids: string[]) => {
    console.log('文件ID:', ids);
  };

  return (
    <div style={{ padding: '20px', maxWidth: '600px' }}>
      <h2>项目文件上传器(OSS 版)</h2>
      <AliUploader
        accept=".doc,.docx,.pdf,.png,.jpg"
        uploadName="上传项目文件到 OSS"
        maxCount={5}
        maxBytes={10}
        multiple={true}
        listType="picture"
        showUploadList={true}
        ossConfig={ossConfig}
        onChange={handleChange}
        onSuccess={handleSuccess}
        filedIds={handleIds}
        extraTip={
          <p style={{ color: '#999' }}>
            支持拖拽批量上传,直接存至 OSS,最多5个文件
          </p>
        }
      />
    </div>
  );
};

export default App;

 

使用效果

  1. 初始化:组件加载时从 OSS 获取已有文件列表。
  2. 上传:拖入 doc.pdf 和 image.png,文件上传至 OSS,显示进度。
  3. 分组:分为“图片”和“文档”,可按时间或名称排序。
  4. 批量编辑:选中文件,添加备注“会议资料”。
  5. 删除:移除文件,同时清理 OSS。
  6. 链接返回:控制台打印 OSS 链接,如 https://web-xiaoyao.oss-cn-hangzhou.aliyuncs.com/xxx.png。

组件解析:云端快递的魔法

  1. OSS 集成
    • uploadToOSS 上传文件,返回 OSS 链接。
    • getOSSList 初始化云端文件。
    • delfileFromOSS 删除云端文件。
  2. 上传体验
    • 多线程上传,进度实时更新。
    • 拖拽支持,操作直观。
  3. 文件管理
    • 分组、排序、批量编辑,井然有序。
    • 预览和删除,功能齐全。

使用场景与扩展

场景

  • 项目协作:上传文件到 OSS,共享链接。
  • 内容管理:批量上传图片或文档。
  • 云备份:自动同步到云端。

总结:你的云端“快递员”

AliUploader 就像一个“云端快递员”,把文件快速送上 Aliyun OSS,返回链接随时取用。通过优化,我们让它更健壮、更高效,用“项目文件上传器”展示了它的实力。试着拖几个文件进去跑跑看,或者丢进你的项目玩一玩吧!有其他需求或创意?欢迎留言一起聊聊!

关键词:React 文件上传组件、AliUploader OSS、云存储链接、前端文件管理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2328881.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

群体智能优化算法-变色龙优化算法(Chameleon Swarm Algorithm, CSA,含Matlab源代码)

摘要 变色龙优化算法&#xff08;Chameleon Swarm Algorithm, CSA&#xff09;是一种受变色龙行为启发的群体智能优化算法。该算法模拟了变色龙在自然界中通过变换颜色来适应环境的能力&#xff0c;以此为基础&#xff0c;设计了一个适应性强、搜索能力广泛的优化算法。CSA 通…

使用 React 和 Konva 实现一个在线画板组件

文章目录 一、前言二、Konva.js 介绍三、创建 React 画板项目3.1 安装依赖3.2 创建 CanvasBoard 组件 四、增加画布控制功能4.1 清空画布4.2 撤销 & 重做功能 五、增加颜色和画笔大小选择5.1 选择颜色5.2 选择画笔大小 六、最终效果七、总结 一、前言 在线画板是许多应用&…

NVR接入录像回放平台EasyCVR视频系统守护舌尖上的安全,打造“明厨亮灶”云监管平台

一、方案背景 近年来&#xff0c;餐饮行业食品安全和卫生等问题频发&#xff0c;比如后厨卫生脏乱差等&#xff0c;持续引发关注&#xff0c;这些事情导致连锁反应&#xff0c;使其收益遭受损失。同时&#xff0c;给消费者造成了心理和生理上的伤害。 加强餐饮行业的监管成为…

Cribl 导入文件来检查pipeline 的设定规则(eval 等)

Cribl 导入文件来检查pipeline 的设定规则(eval 等) 从这个页面先下载,或者copy 内容来创建pipeline: Reducing Windows XML Events | Cribl Docs

一周学会Pandas2 Python数据处理与分析-Jupyter Notebook安装

锋哥原创的Pandas2 Python数据处理与分析 视频教程&#xff1a; 2025版 Pandas2 Python数据处理与分析 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili Jupyter (Project Jupyter | Home&#xff09;项目是一个非营利性开源项目&#xff0c;于2014年由IPython项目中诞生…

deepseek v3-0324 Markdown 编辑器 HTML

Markdown 编辑器 HTML 以下是一个美观的 Markdown 编辑器 HTML 页面&#xff0c;支持多种主题切换和实时预览功能&#xff1a; <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&q…

视频设备轨迹回放平台EasyCVR如何搭建公共娱乐场所远程视频监控系统

一、背景介绍 由于KTV、酒吧、足疗店等服务场所人员流动频繁、环境复杂&#xff0c;一直是治安管理的重点区域。为有效打击 “黄赌毒”、打架斗殴、寻衅滋事等违法犯罪的活动&#xff0c;打造安全有序的娱乐消费环境&#xff0c;我国相关部门将加大对这类场所的清查与管控力度…

网络安全基础知识总结

什么是网络安全 采取必要措施&#xff0c;来防范对网络的攻击&#xff0c;侵入&#xff0c;干扰&#xff0c;破坏和非法使用&#xff0c;以及防范一些意外事故&#xff0c;使得网络处于稳定可靠运行的状态&#xff0c;保障网络数据的完整性、保密性、可用性的能力(CIA)。 举例…

【工具】在 Visual Studio 中使用 Dotfuscator 对“C# 类库(DLL)或应用程序(EXE)”进行混淆

在 Visual Studio 中使用 Dotfuscator 进行混淆 Dotfuscator 是 Visual Studio 自带的混淆工具&#xff08;Dotfuscator Community Edition&#xff0c;简称 CE&#xff09;。它可以混淆 C# 类库&#xff08;DLL&#xff09;或应用程序&#xff08;EXE&#xff09;&#xff0c…

积分赛——获取环境温度

设计要求 从DS18B20温度传感器上获取环境温度&#xff0c;并将其温度值显示到数码管上&#xff08;保留两位小数&#xff09;。 当“S4”定义为发送按键&#xff0c;按键S4按下时&#xff0c;串口向PC端发送当前采集的温度值&#xff1b; 串口发送格式&#xff1a; Temp:26.…

Xilinx系列FPGA实现HDMI2.1视频收发,支持8K@60Hz分辨率,提供2套工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐我已有的所有工程源码总目录----方便你快速找到自己喜欢的项目我已有的4K/8K视频处理解决方案我已有的FPGA图像处理方案 3、详细设计方案设计框图硬件设计架构本HDMI2.1性能参数8K视频输入源Video PHY ControllerHDMI 2.1 Receive…

如何把网页文章转为pdf保存

fnF12调出右边网页端的控制台 在下面输入代码 1、转CSDN上的文章 (function(){ use strict;var articleBox $("div.article_content");articleBox.removeAttr("style");var head_str ""; var foot_str ""; var olde…

自然语言处理|如何用少样本技术提升低资源语言处理?

一、引言 在全球化的背景下&#xff0c;自然语言处理&#xff08;NLP&#xff09;技术取得了显著进展&#xff0c;为人们的生活和工作提供了便利。然而&#xff0c;大多数 NLP 研究和应用集中在少数高资源语言上&#xff0c;如英语和中文。据统计&#xff0c;全球存在超过 700…

系统安全——文件监控-FileMonitor

namespace FileSystemWatcherDemo {public partial class Form1 : Form{ public Form1(){InitializeComponent();UsingFileSystemWatcher();} /// <summary>/// 使用FileSystemWatcher方法/// </summary>void UsingFileSystemWatcher(){//6.2//FileSystemWa…

07-01-自考数据结构(20331)- 排序-内部排序知识点

内部排序算法是数据结构核心内容,主要包括插入类(直接插入、希尔)、交换类(冒泡、快速)、选择类(简单选择、堆)、归并和基数五大类排序方法。 知识拓扑 知识点介绍 直接插入排序 定义:将每个待排序元素插入到已排序序列的适当位置 算法步骤: 从第二个元素开始遍历…

【AI学习】MCP的简单快速理解

最近&#xff0c;AI界最火热的恐怕就是MCP了。作为一个新的知识点&#xff0c;学习的开始&#xff0c;先摘录一些信息&#xff0c;从发展历程、通俗介绍到具体案例&#xff0c;这样可以快速理解MCP。 MCP发展历程 来自i陆三金 Anthropic 开发者关系负责人 Alex Albert&#…

单机快速部署开源、免费的分布式任务调度系统——DolphinScheduler

看了DolphinScheduler的介绍&#xff0c;不知道有没有引起你的兴趣&#xff0c;有没有想要上手体验一番呢。本文则主要为大家介绍DolphinScheduler的单机部署方式&#xff0c;方便大家快速体验。 环境准备 需要Java环境&#xff0c;这是一个老生常谈的问题&#xff0c;关于Ja…

【大模型系列篇】大模型基建工程:基于 FastAPI 自动构建 SSE MCP 服务器

今天我们将使用FastAPI来构建 MCP 服务器&#xff0c;Anthropic 推出的这个MCP 协议&#xff0c;目的是让 AI 代理和你的应用程序之间的对话变得更顺畅、更清晰。FastAPI 基于 Starlette 和 Uvicorn&#xff0c;采用异步编程模型&#xff0c;可轻松处理高并发请求&#xff0c;尤…

springcloud configClient获取configServer信息失败导致启动configClient注入失败报错解决

目录 一、问题现象 二、解决方案 三、运行结果 四、代码地址 一、问题现象 springcloud configClient获取configServer信息失败导致启动configClient注入失败 报错堆栈信息 org.springframework.beans.factory.BeanCreationException: Error creating bean with name scop…

HarmonyOS-ArkUI Rcp模块类关系梳理

前言 本文重点解决的是&#xff0c;按照官网学习路径学习Tcp模块内容时&#xff0c;越看越混乱的问题。仿照官网案例&#xff0c;书写代码时&#xff0c;产生的各种疑惑。比如&#xff0c;类与类之间的关系&#xff0c;各种配置信息究竟有多少&#xff0c;为什么越写越混乱。那…