前端 + AI 进阶学习路线|Week 1-2:流式体验优化
Day 5:多模态输入初探 —— 图片上传与实时预览(完整版)
学习时间:2025年12月29日(星期一)
关键词:图片上传、拖拽上传、截图粘贴、Clipboard API、File API、Canvas 预览
🎯 今日目标
- 实现 三种图片上传方式:文件选择器、拖拽、截图粘贴(Ctrl+V / Cmd+V)
- 支持 本地预览(无需后端)
- 构建可复用
ImageUpload组件,输出File对象供后续 AI 分析
📁 文件结构
src/
├── components/
│ └── ImageUpload.jsx # 核心上传组件
└── App.jsx # 主应用集成
✅ 本日不依赖第三方 UI 库,纯 React + 原生 Web API
🔧 完整代码(可直接复制粘贴)
1. src/components/ImageUpload.jsx
// 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;}// 生成本地预览 URLconst 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 (<divstyle={{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 */}<inputtype="file"ref={fileInputRef}onChange={handleFileChange}accept={accept}style={{ display: 'none' }}aria-hidden="true"/>{previewUrl ? (<div><imgsrc={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;
2. src/App.jsx
// 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><buttononClick={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;
📦 依赖说明
本日 无需额外安装 npm 包,仅使用:
- React(已由
create-react-app提供) - 原生 Web API:
FileReader,URL.createObjectURL,Clipboard API,Drag Event
✅ 你可直接创建新项目并复制上述两个文件运行
🚀 本地运行步骤
# 1. 创建项目
npx create-react-app day05-image-upload
cd day05-image-upload# 2. 替换 src/App.jsx 和 src/components/ImageUpload.jsx(先创建 components 目录)# 3. 启动
npm start
✅ 功能验证清单
| 功能 | 操作 | 预期结果 |
|---|---|---|
| 文件选择 | 点击区域 → 选择图片 | 显示预览 |
| 拖拽上传 | 拖一张图到区域 | 自动上传并预览 |
| 截图粘贴 | 截图 → 切换到页面 → Ctrl+V | 图片自动粘贴上传 |
| 非图片文件 | 上传 PDF | 弹出警告 |
| 重复上传 | 上传后再次上传 | 预览更新,无内存泄漏 |
💡 扩展建议(后续使用)
- 压缩图片:在
handleFile中加入 Canvas 压缩(为 Day 6 LLaVA 准备) - 多图支持:将
selectedFile改为数组,accept加multiple - Base64 转换:用于传给 Ollama(见 Day 6)
📅 明日预告(完整版)
Day 6:图片标注与 AI 视觉分析
- 在 Canvas 上画框 / 圈选
- 调用 Ollama LLaVA 进行流式视觉问答
- 构建“上传 → 标注 → 提问 → AI 回答”完整闭环
所有代码将继续以 完整、可运行 形式提供!