前端 + AI 进阶 Day5:多模态输入初探 —— 图片上传与实时预览

前端 + AI 进阶学习路线|Week 1-2:流式体验优化

Day 5:多模态输入初探 —— 图片上传与实时预览

学习时间:2025年12月29日(星期一)
关键词:图片上传、拖拽上传、截图粘贴、Clipboard API、File API、Canvas 预览


📁 项目文件结构

day05-image-upload/
├── src/
│   ├── components/
│   │   └── ImageUpload.jsx        # 多模态图片上传组件(支持拖拽/粘贴)
│   └── App.jsx                    # 主应用入口
└── public/

✅ 本日不依赖第三方 UI 库,仅使用 React + 原生 Web API


🎯 今日学习目标

  1. 实现 三种图片上传方式:文件选择器、拖拽、截图粘贴(Ctrl+V / Cmd+V)
  2. 支持 本地预览(无需后端,使用 URL.createObjectURL
  3. 构建可复用 ImageUpload 组件,输出 File 对象供后续 AI 分析
  4. 为 Day 6 的视觉 AI(如 LLaVA)准备输入素材

💡 为什么多模态输入对 AI 应用至关重要?

现代 AI 不再只是“文字聊天”:

  • 用户可上传 截图 让 AI 解释错误信息
  • 上传 商品图 让 AI 生成描述
  • 粘贴 设计稿 让 AI 提建议

✅ 前端需成为 多模态入口:接收图片、音频、PDF 等,并预处理后传给 AI
今天,我们从 图片上传 开始!


📚 核心 Web API

功能 API
选择文件 <input type="file" accept="image/*">
拖拽上传 dragover / drop 事件
截图粘贴 paste 事件 + clipboardData.items
本地预览 URL.createObjectURL(file)
释放内存 URL.revokeObjectURL(url)(组件卸载时调用)

⚠️ 注意:createObjectURL 会占用内存,务必在组件销毁时清理


🔧 动手实践:构建多功能图片上传组件

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

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

步骤 2:编写图片上传组件

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

/**
 * 多模态图片上传组件
 * 支持:文件选择 / 拖拽 / 截图粘贴(Ctrl+V)
 * 输出:File 对象(供 AI 分析)
 */
const ImageUpload = ({ onFileSelect, accept = "image/*" }) => {
  const [previewUrl, setPreviewUrl] = useState(null);
  const [isDragging, setIsDragging] = useState(false);
  const fileInputRef = useRef(null);

  // 清理 object URL 避免内存泄漏
  useEffect(() => {
    return () => {
      if (previewUrl) {
        URL.revokeObjectURL(previewUrl);
      }
    };
  }, [previewUrl]);

  // 处理文件逻辑(校验 + 预览 + 回调)
  const handleFile = useCallback((file) => {
    if (!file) return;

    // 类型校验
    if (accept === "image/*" && !file.type.startsWith('image/')) {
      alert('⚠️ 请上传图片文件(支持 PNG、JPG、GIF 等)');
      return;
    }

    // 生成本地预览 URL
    const url = URL.createObjectURL(file);
    setPreviewUrl(url);

    // 通知父组件
    onFileSelect?.(file);
  }, [onFileSelect, accept]);

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

  // 文件选择器变化
  const handleFileChange = (e) => {
    const file = e.target.files?.[0];
    handleFile(file);
    // 重置 input value,允许重复上传同名文件
    e.target.value = '';
  };

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

  const handleDragLeave = () => {
    setIsDragging(false);
  };

  const handleDrop = (e) => {
    e.preventDefault();
    setIsDragging(false);
    const file = e.dataTransfer.files?.[0];
    handleFile(file);
  };

  // 截图粘贴(全局)
  const handlePaste = (e) => {
    const items = e.clipboardData?.items;
    if (!items) return;

    // 遍历剪贴板项,查找图片
    for (let i = 0; i < items.length; i++) {
      if (items[i].type.indexOf('image') === 0) {
        const blob = items[i].getAsFile();
        if (blob) {
          // 转为 File 对象(带文件名)
          const file = new File([blob], `pasted-${Date.now()}.png`, {
            type: blob.type,
          });
          handleFile(file);
          break; // 只处理第一张图
        }
      }
    }
  };

  return (
    <div
      style={{
        padding: '24px',
        border: '2px dashed #d9d9d9',
        borderRadius: '12px',
        textAlign: 'center',
        backgroundColor: isDragging ? '#e6f7ff' : '#fafafa',
        transition: 'background 0.2s ease',
        cursor: 'pointer',
        position: 'relative',
      }}
      onDragOver={handleDragOver}
      onDragLeave={handleDragLeave}
      onDrop={handleDrop}
      onClick={handleSelectClick}
      onPaste={handlePaste}
      tabIndex={0} // 使 div 可聚焦以接收 paste 事件
      role="button"
      aria-label="上传图片区域"
    >
      {/* 隐藏的原生文件 input */}
      <input
        type="file"
        ref={fileInputRef}
        onChange={handleFileChange}
        accept={accept}
        style={{ display: 'none' }}
        aria-hidden="true"
      />

      {previewUrl ? (
        <div>
          <img
            src={previewUrl}
            alt="上传预览"
            style={{
              maxWidth: '100%',
              maxHeight: '300px',
              borderRadius: '8px',
              boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
              objectFit: 'contain',
            }}
          />
          <p style={{ marginTop: '12px', color: '#666', fontSize: '14px' }}>
            ✅ 已上传,点击重新选择
          </p>
        </div>
      ) : (
        <div>
          <div style={{ fontSize: '20px', marginBottom: '8px' }}>🖼️</div>
          <div style={{ fontSize: '16px', fontWeight: '500', color: '#333' }}>
            拖拽图片到此处,或点击上传
          </div>
          <div style={{ fontSize: '14px', color: '#888', marginTop: '6px' }}>
            也支持截图后直接 <strong>Ctrl+V (Windows) / Cmd+V (Mac)</strong> 粘贴!
          </div>
        </div>
      )}
    </div>
  );
};

export default ImageUpload;

步骤 3:在 App 中集成并展示文件信息

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

function App() {
  const [selectedFile, setSelectedFile] = useState(null);

  const handleFileSelect = (file) => {
    console.log('✅ 选中的文件:', {
      name: file.name,
      size: `${(file.size / 1024).toFixed(2)} KB`,
      type: file.type,
    });
    setSelectedFile(file);
  };

  // 模拟发送给 AI(实际应调用视觉模型)
  const handleSendToAI = () => {
    if (!selectedFile) {
      alert('请先上传一张图片!');
      return;
    }
    alert(`📤 图片 "${selectedFile.name}" 已准备好发送给 AI 分析!`);
    // 后续可在此调用:
    // streamVisualAnalysis({ imageFile: selectedFile, prompt: "..." })
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'Inter, -apple-system, sans-serif', maxWidth: '700px', margin: '0 auto' }}>
      <header style={{ textAlign: 'center', marginBottom: '32px' }}>
        <h1 style={{ fontSize: '28px', fontWeight: '700', color: '#1d1d1f' }}>
          多模态输入:图片上传
        </h1>
        <p style={{ color: '#666', fontSize: '16px' }}>
          为 AI 视觉分析准备输入素材
        </p>
      </header>

      <main>
        <ImageUpload onFileSelect={handleFileSelect} />

        {selectedFile && (
          <div style={{ marginTop: '24px', padding: '16px', backgroundColor: '#f8f9fa', borderRadius: '12px', border: '1px solid #e9ecef' }}>
            <h3 style={{ margin: '0 0 12px 0', fontSize: '18px', color: '#333' }}>📄 文件信息</h3>
            <ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
              <li style={{ marginBottom: '8px' }}>
                <strong>文件名:</strong> {selectedFile.name}
              </li>
              <li style={{ marginBottom: '8px' }}>
                <strong>大小:</strong> {(selectedFile.size / 1024).toFixed(2)} KB
              </li>
              <li>
                <strong>类型:</strong> {selectedFile.type}
              </li>
            </ul>

            <button
              onClick={handleSendToAI}
              style={{
                marginTop: '16px',
                padding: '10px 24px',
                backgroundColor: '#1890ff',
                color: 'white',
                border: 'none',
                borderRadius: '6px',
                fontSize: '16px',
                cursor: 'pointer',
                fontWeight: '500',
                transition: 'background 0.2s',
              }}
              onMouseOver={(e) => e.target.style.backgroundColor = '#40a9ff'}
              onMouseOut={(e) => e.target.style.backgroundColor = '#1890ff'}
            >
              🤖 发送给 AI 分析
            </button>
          </div>
        )}
      </main>

      <footer style={{ marginTop: '40px', textAlign: 'center', color: '#888', fontSize: '14px' }}>
        Day 5 · 前端 + AI 实战 · 支持拖拽、粘贴、选择三种上传方式
      </footer>
    </div>
  );
}

export default App;

✅ 效果验证

功能 操作 预期结果
文件选择 点击区域 → 选择图片 显示预览
拖拽上传 拖一张图到区域 自动上传并预览
截图粘贴 截图 → 切换到页面 → Ctrl+V 图片自动粘贴上传
非图片文件 上传 PDF 弹出警告
重复上传 上传后再次上传 预览更新,无内存泄漏

🤔 思考与延伸

  1. 大图性能:如何限制上传尺寸(如 >5MB 提示)?
    → 在 handleFile 中检查 file.size

  2. 多图上传:如何支持一次上传多张图?
    → 修改 acceptmultiple,状态改为数组

  3. AI 集成准备:如何将图片转为 Base64 供 LLaVA 使用?
    → 使用 FileReader 读取(Day 6 将实现)

提示:为适配视觉模型(如 LLaVA),建议后续加入 图片压缩(Canvas 缩放)


📅 明日预告

Day 6:图片标注与 AI 视觉分析

  • 在 Canvas 上实现 画框、圈选、文字标注
  • 调用 Ollama LLaVA 进行流式视觉问答
  • 构建“上传 → 标注 → 提问 → AI 回答”完整闭环

✍️ 小结

今天,我们让 AI 聊天界面“看得见”了!通过拖拽、粘贴、选择三种方式,用户可以轻松上传图片,前端实时预览,为后续的视觉理解打下坚实基础。多模态交互,从此刻开始。

💬 实践提示:如果截图粘贴无效,请确保点击上传区域使其获得焦点后再粘贴。欢迎分享你的多模态交互设计!

posted @ 2025-12-29 14:10  XiaoZhengTou  阅读(17)  评论(0)    收藏  举报