前端 + AI 进阶 Day7: 批量上传与进度管理

前端 + AI 进阶学习路线|Week 3-4:多模态前端交互

Day 7:批量上传与进度管理

学习时间:2025年12月31日(星期三)
关键词:批量上传、拖拽多文件、上传进度条、文件队列、进度模拟


📁 项目文件结构

day07-batch-upload/
├── src/
│   ├── components/
│   │   └── BatchUploadArea.jsx     # 批量上传区域(拖拽/选择 + 进度)
│   └── App.jsx                     # 主应用集成(文件列表 + 状态)
└── public/

✅ 本日聚焦 多文件并行上传体验,为“批量 AI 分析”打下基础


🎯 今日学习目标

  1. 支持 多文件拖拽/选择上传(一次上传多张图片)
  2. 显示 每个文件的独立上传进度条(模拟真实进度)
  3. 构建 文件队列管理 逻辑(上传中 / 成功 / 失败)
  4. 为后续“批量图片分析”提供输入管道

💡 为什么需要批量上传?

在真实 AI 工作流中,用户常需:

  • 上传 多张截图 让 AI 对比分析
  • 批量提交 商品图片 生成描述
  • 一次上传 整套设计稿 进行 UI 评审

✅ 单文件上传效率低下,批量 + 进度反馈 是专业体验的标配


📚 核心设计思路

功能 实现方式
多文件选择 <input multiple accept="image/*">
拖拽多文件 drop 事件 + dataTransfer.files
进度模拟 setTimeout + 随机进度增长(真实场景用 XMLHttpRequestfetch 上传)
状态管理 每个文件独立状态:pending / uploading / success / error

⚠️ 注意:前端无法获取真实上传进度(除非对接真实 API),但可 模拟流畅进度 提升体验


🔧 动手实践:构建批量上传组件

步骤 1:创建项目(无需额外依赖)

npx create-react-app day07-batch-upload
cd day07-batch-upload
# 无需安装 npm 包,纯原生实现

步骤 2:编写批量上传组件

// src/components/BatchUploadArea.jsx
import { useState, useRef, useCallback } from 'react';

const BatchUploadArea = ({ onFilesUploaded }) => {
  const [files, setFiles] = useState([]); // { file, id, progress, status }
  const fileInputRef = useRef(null);

  // 生成唯一 ID
  const generateId = () => Math.random().toString(36).substr(2, 9);

  // 处理文件列表(支持多文件)
  const handleFiles = useCallback((fileList) => {
    const newFiles = Array.from(fileList)
      .filter(file => file.type.startsWith('image/'))
      .map(file => ({
        id: generateId(),
        file,
        name: file.name,
        size: file.size,
        progress: 0,
        status: 'pending', // pending | uploading | success | error
      }));

    if (newFiles.length === 0) {
      alert('请选择图片文件(PNG/JPG/GIF)');
      return;
    }

    setFiles(prev => [...prev, ...newFiles]);

    // 启动模拟上传
    newFiles.forEach(fileObj => simulateUpload(fileObj.id));
  }, []);

  // 模拟上传过程(真实场景替换为 fetch/XHR)
  const simulateUpload = (id) => {
    setFiles(prev => prev.map(f => 
      f.id === id ? { ...f, status: 'uploading', progress: 0 } : f
    ));

    let progress = 0;
    const interval = setInterval(() => {
      progress += Math.floor(Math.random() * 10) + 5; // 随机增长
      if (progress >= 100) {
        progress = 100;
        clearInterval(interval);
        setFiles(prev => {
          const updated = prev.map(f => 
            f.id === id ? { ...f, status: 'success', progress } : f
          );
          // 通知父组件上传完成
          const completedFile = updated.find(f => f.id === id);
          onFilesUploaded?.(completedFile.file);
          return updated;
        });
      } else {
        setFiles(prev => prev.map(f => 
          f.id === id ? { ...f, progress } : f
        ));
      }
    }, 200);
  };

  // 文件选择器
  const handleSelectClick = () => {
    fileInputRef.current?.click();
  };

  const handleFileChange = (e) => {
    handleFiles(e.target.files);
    e.target.value = ''; // 允许重复选择同名文件
  };

  // 拖拽事件
  const handleDragOver = (e) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'copy';
  };

  const handleDrop = (e) => {
    e.preventDefault();
    handleFiles(e.dataTransfer.files);
  };

  // 移除单个文件
  const removeFile = (id) => {
    setFiles(prev => prev.filter(f => f.id !== id));
  };

  return (
    <div>
      {/* 上传区域 */}
      <div
        style={{
          padding: '24px',
          border: '2px dashed #d9d9d9',
          borderRadius: '12px',
          textAlign: 'center',
          backgroundColor: '#fafafa',
          cursor: 'pointer',
          marginBottom: '20px',
        }}
        onClick={handleSelectClick}
        onDragOver={handleDragOver}
        onDrop={handleDrop}
      >
        <input
          type="file"
          ref={fileInputRef}
          onChange={handleFileChange}
          multiple
          accept="image/*"
          style={{ display: 'none' }}
        />
        <div style={{ fontSize: '20px', marginBottom: '8px' }}>📁</div>
        <div style={{ fontSize: '16px', fontWeight: '500', color: '#333' }}>
          拖拽多张图片到此处,或点击选择
        </div>
        <div style={{ fontSize: '14px', color: '#888', marginTop: '6px' }}>
          支持批量上传,自动显示上传进度
        </div>
      </div>

      {/* 文件列表 */}
      {files.length > 0 && (
        <div style={{ marginTop: '20px' }}>
          <h3 style={{ marginBottom: '12px', fontSize: '18px' }}>上传队列 ({files.length} 个文件)</h3>
          <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
            {files.map((fileObj) => (
              <div
                key={fileObj.id}
                style={{
                  padding: '12px',
                  border: '1px solid #e8e8e8',
                  borderRadius: '8px',
                  backgroundColor: '#fff',
                }}
              >
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
                  <div>
                    <strong>{fileObj.name}</strong>
                    <span style={{ fontSize: '12px', color: '#888', marginLeft: '8px' }}>
                      {(fileObj.size / 1024).toFixed(1)} KB
                    </span>
                  </div>
                  <button
                    onClick={() => removeFile(fileObj.id)}
                    style={{
                      background: 'none',
                      border: 'none',
                      color: '#ff4d4f',
                      cursor: 'pointer',
                      fontSize: '18px',
                    }}
                  >
                    &times;
                  </button>
                </div>

                {/* 进度条 */}
                <div style={{ height: '6px', backgroundColor: '#f0f0f0', borderRadius: '3px', overflow: 'hidden' }}>
                  <div
                    style={{
                      height: '100%',
                      width: `${fileObj.progress}%`,
                      backgroundColor: 
                        fileObj.status === 'success' ? '#52c41a' :
                        fileObj.status === 'error' ? '#ff4d4f' :
                        '#1890ff',
                      transition: 'width 0.3s ease',
                    }}
                  />
                </div>

                {/* 状态标签 */}
                <div style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
                  {fileObj.status === 'pending' && '等待上传...'}
                  {fileObj.status === 'uploading' && `上传中... ${fileObj.progress}%`}
                  {fileObj.status === 'success' && '✅ 上传成功'}
                  {fileObj.status === 'error' && '❌ 上传失败'}
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

export default BatchUploadArea;

步骤 3:在 App 中集成并展示上传结果

// src/App.jsx
import { useState } from 'react';
import BatchUploadArea from './components/BatchUploadArea';

function App() {
  const [uploadedFiles, setUploadedFiles] = useState([]);

  const handleFileUploaded = (file) => {
    console.log('✅ 文件上传成功:', file.name);
    setUploadedFiles(prev => [...prev, file]);
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'Inter, -apple-system, sans-serif', maxWidth: '800px', margin: '0 auto' }}>
      <header style={{ textAlign: 'center', marginBottom: '32px' }}>
        <h1 style={{ fontSize: '28px', fontWeight: '700', color: '#1d1d1f' }}>
          批量图片上传与进度管理
        </h1>
        <p style={{ color: '#666', fontSize: '16px' }}>
          支持多文件拖拽 + 独立进度条
        </p>
      </header>

      <main>
        <BatchUploadArea onFilesUploaded={handleFileUploaded} />

        {uploadedFiles.length > 0 && (
          <div style={{ marginTop: '30px', padding: '16px', backgroundColor: '#f8f9fa', borderRadius: '12px', border: '1px solid #e9ecef' }}>
            <h3 style={{ margin: '0 0 12px 0', fontSize: '18px', color: '#333' }}>
              ✅ 已上传文件 ({uploadedFiles.length} 个)
            </h3>
            <ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
              {uploadedFiles.map((file, index) => (
                <li key={index} style={{ marginBottom: '6px' }}>
                  • {file.name} ({(file.size / 1024).toFixed(1)} KB)
                </li>
              ))}
            </ul>
            <button
              onClick={() => {
                alert('📤 将批量发送给 AI 分析!(Day 8 实现)');
              }}
              style={{
                marginTop: '16px',
                padding: '10px 24px',
                backgroundColor: '#52c41a',
                color: 'white',
                border: 'none',
                borderRadius: '6px',
                fontSize: '16px',
                cursor: 'pointer',
              }}
            >
              🤖 批量发送给 AI 分析
            </button>
          </div>
        )}
      </main>

      <footer style={{ marginTop: '40px', textAlign: 'center', color: '#888', fontSize: '14px' }}>
        Day 7 · 前端 + AI 实战 · 支持多文件拖拽上传与进度反馈
      </footer>
    </div>
  );
}

export default App;

✅ 效果验证

功能 操作 预期结果
多文件选择 点击区域 → 选择多张图片 所有图片进入队列
拖拽多文件 拖 3 张图到区域 3 个进度条同时开始
进度模拟 观察进度条 随机增长至 100%,变为绿色
状态反馈 上传成功 显示 ✅ 上传成功
移除文件 点击 × 文件从队列移除

🤔 思考与延伸

  1. 真实上传:如何对接后端 API 获取真实进度?
    → 使用 XMLHttpRequestonprogress 事件

  2. 并发控制:如何限制同时上传数量(如最多 3 个)?
    → 用队列 + Promise 控制并发

  3. 失败重试:如何实现“上传失败 → 重试”按钮?
    → 为每个文件添加 retry 方法

💡 为 AI 准备:上传完成后,可将文件列表传给 Day 8 的批量分析模块


📅 明日预告

Day 8:批量图片 AI 分析

  • 调用多模态模型(LLaVA)批量分析图片
  • 并行请求 + 结果聚合展示
  • 构建“上传 → 分析 → 报告”完整工作流

✍️ 小结

今天,我们让前端上传体验从“单点”走向“批量”!通过独立进度条、状态管理和拖拽多文件支持,用户可高效提交大量素材,为后续的批量 AI 分析铺平道路。批量处理,是专业工具的标志。

💬 实践提示:真实项目中,建议用 XMLHttpRequest 替换 setTimeout 以获取真实进度。欢迎分享你的批量上传交互设计!

posted @ 2025-12-29 15:06  XiaoZhengTou  阅读(35)  评论(0)    收藏  举报