wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 1 | // src/components/UploadPage.jsx |
| 2 | |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 3 | import React, { useState } from 'react' |
| 4 | import { Image, Video } from 'lucide-react' |
| 5 | |
| 6 | |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 7 | /** |
| 8 | * @param {Object} props |
| 9 | * @param {(files: File[]) => void} [props.onComplete] 上传完成后回调,接收 File 数组 |
| 10 | */ |
| 11 | export default function UploadPage({ onComplete }) { |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 12 | const [activeTab, setActiveTab] = useState('image') |
| 13 | const [isDragOver, setIsDragOver] = useState(false) |
| 14 | const [isUploading, setIsUploading] = useState(false) |
| 15 | const [uploadedFiles, setUploadedFiles] = useState([]) |
| 16 | const [uploadProgress, setUploadProgress] = useState(0) |
| 17 | |
| 18 | const validateFiles = files => { |
| 19 | const imgTypes = ['image/jpeg','image/jpg','image/png','image/webp'] |
| 20 | const vidTypes = ['video/mp4','video/mov','video/avi'] |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 21 | const types = activeTab === 'video' ? vidTypes : imgTypes |
| 22 | const max = activeTab === 'video' |
| 23 | ? 2 * 1024 * 1024 * 1024 |
| 24 | : 32 * 1024 * 1024 |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 25 | |
| 26 | const invalid = files.filter(f => !types.includes(f.type) || f.size > max) |
| 27 | if (invalid.length) { |
| 28 | alert(`发现 ${invalid.length} 个无效文件,请检查文件格式和大小`) |
| 29 | return false |
| 30 | } |
| 31 | return true |
| 32 | } |
| 33 | |
| 34 | const simulateUpload = files => { |
| 35 | setIsUploading(true) |
| 36 | setUploadProgress(0) |
| 37 | setUploadedFiles(files) |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 38 | |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 39 | const iv = setInterval(() => { |
| 40 | setUploadProgress(p => { |
| 41 | if (p >= 100) { |
| 42 | clearInterval(iv) |
| 43 | setIsUploading(false) |
| 44 | alert(`成功上传了 ${files.length} 个文件`) |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 45 | // 上传完成后回调 |
| 46 | if (typeof onComplete === 'function') { |
| 47 | onComplete(files) |
| 48 | } |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 49 | return 100 |
| 50 | } |
| 51 | return p + 10 |
| 52 | }) |
| 53 | }, 200) |
| 54 | } |
| 55 | |
| 56 | const handleFileUpload = () => { |
| 57 | if (isUploading) return |
| 58 | const input = document.createElement('input') |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 59 | input.type = 'file' |
| 60 | input.accept = activeTab === 'video' ? 'video/*' : 'image/*' |
| 61 | input.multiple = activeTab === 'image' |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 62 | input.onchange = e => { |
| 63 | const files = Array.from(e.target.files) |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 64 | if (files.length > 0 && validateFiles(files)) { |
| 65 | simulateUpload(files) |
| 66 | } |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 67 | } |
| 68 | input.click() |
| 69 | } |
| 70 | |
| 71 | const handleDragOver = e => { e.preventDefault(); e.stopPropagation(); setIsDragOver(true) } |
| 72 | const handleDragLeave = e => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false) } |
| 73 | const handleDrop = e => { |
| 74 | e.preventDefault(); e.stopPropagation(); setIsDragOver(false) |
| 75 | if (isUploading) return |
| 76 | const files = Array.from(e.dataTransfer.files) |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 77 | if (files.length > 0 && validateFiles(files)) { |
| 78 | simulateUpload(files) |
| 79 | } |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 80 | } |
| 81 | |
| 82 | const clearFiles = () => setUploadedFiles([]) |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 83 | const removeFile = idx => setUploadedFiles(prev => prev.filter((_, i) => i !== idx)) |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 84 | |
| 85 | return ( |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 86 | <div className="upload-page"> |
| 87 | {/* 上传类型切换 */} |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 88 | <div className="upload-tabs"> |
| 89 | <button |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 90 | className={`upload-tab${activeTab === 'video' ? ' active' : ''}`} |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 91 | onClick={() => setActiveTab('video')} |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 92 | > |
| 93 | 上传视频 |
| 94 | </button> |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 95 | <button |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 96 | className={`upload-tab${activeTab === 'image' ? ' active' : ''}`} |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 97 | onClick={() => setActiveTab('image')} |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 98 | > |
| 99 | 上传图文 |
| 100 | </button> |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 101 | </div> |
| 102 | |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 103 | {/* 拖拽/点击上传区域 */} |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 104 | <div |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 105 | className={`upload-area${isDragOver ? ' drag-over' : ''}`} |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 106 | onDragOver={handleDragOver} |
| 107 | onDragLeave={handleDragLeave} |
| 108 | onDrop={handleDrop} |
| 109 | > |
| 110 | <div className="upload-icon"> |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 111 | {activeTab === 'video' ? <Video size={48} /> : <Image size={48} />} |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 112 | </div> |
| 113 | <h2 className="upload-title"> |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 114 | {activeTab === 'video' |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 115 | ? '拖拽视频到此处或点击上传' |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 116 | : '拖拽图片到此处或点击上传'} |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 117 | </h2> |
| 118 | <p className="upload-subtitle">(需支持上传格式)</p> |
| 119 | <button |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 120 | className={`upload-btn${isUploading ? ' uploading' : ''}`} |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 121 | onClick={handleFileUpload} |
| 122 | disabled={isUploading} |
| 123 | > |
| 124 | {isUploading |
| 125 | ? `上传中... ${uploadProgress}%` |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 126 | : activeTab === 'video' |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 127 | ? '上传视频' |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 128 | : '上传图片'} |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 129 | </button> |
| 130 | |
| 131 | {isUploading && ( |
| 132 | <div className="progress-container"> |
| 133 | <div className="progress-bar"> |
| 134 | <div |
| 135 | className="progress-fill" |
| 136 | style={{ width: `${uploadProgress}%` }} |
| 137 | /> |
| 138 | </div> |
| 139 | <div className="progress-text">{uploadProgress}%</div> |
| 140 | </div> |
| 141 | )} |
| 142 | </div> |
| 143 | |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 144 | {/* 已上传文件预览 */} |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 145 | {uploadedFiles.length > 0 && ( |
| 146 | <div className="file-preview-area"> |
| 147 | <div className="preview-header"> |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 148 | <h3 className="preview-title"> |
| 149 | 已上传文件 ({uploadedFiles.length}) |
| 150 | </h3> |
| 151 | <button |
| 152 | className="clear-files-btn" |
| 153 | onClick={clearFiles} |
| 154 | > |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 155 | 清除所有 |
| 156 | </button> |
| 157 | </div> |
| 158 | <div className="file-grid"> |
| 159 | {uploadedFiles.map((file, i) => ( |
| 160 | <div key={i} className="file-item"> |
| 161 | <button |
| 162 | className="remove-file-btn" |
| 163 | onClick={() => removeFile(i)} |
| 164 | title="删除文件" |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 165 | > |
| 166 | × |
| 167 | </button> |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 168 | {file.type.startsWith('image/') ? ( |
| 169 | <div className="file-thumbnail"> |
| 170 | <img src={URL.createObjectURL(file)} alt={file.name} /> |
| 171 | </div> |
| 172 | ) : ( |
| 173 | <div className="file-thumbnail video-thumbnail"> |
| 174 | <Video size={24} /> |
| 175 | </div> |
| 176 | )} |
| 177 | <div className="file-info"> |
| 178 | <div className="file-name" title={file.name}> |
| 179 | {file.name.length > 20 |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 180 | ? file.name.slice(0, 17) + '...' |
| 181 | : file.name} |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 182 | </div> |
| 183 | <div className="file-size"> |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 184 | {(file.size / 1024 / 1024).toFixed(2)} MB |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 185 | </div> |
| 186 | </div> |
| 187 | </div> |
| 188 | ))} |
| 189 | </div> |
| 190 | </div> |
| 191 | )} |
| 192 | |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 193 | {/* 上传说明信息 */} |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 194 | <div className="upload-info fade-in"> |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 195 | {activeTab === 'image' ? ( |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 196 | <> |
| 197 | <div className="info-item"> |
| 198 | <h3 className="info-title">图片大小</h3> |
| 199 | <p className="info-desc">最大32MB</p> |
| 200 | </div> |
| 201 | <div className="info-item"> |
| 202 | <h3 className="info-title">图片格式</h3> |
| 203 | <p className="info-desc">png/jpg/jpeg/webp</p> |
| 204 | </div> |
| 205 | <div className="info-item"> |
| 206 | <h3 className="info-title">分辨率</h3> |
| 207 | <p className="info-desc">建议720×960及以上</p> |
| 208 | </div> |
| 209 | </> |
| 210 | ) : ( |
| 211 | <> |
| 212 | <div className="info-item"> |
| 213 | <h3 className="info-title">视频大小</h3> |
| 214 | <p className="info-desc">最大2GB,时长≤5分钟</p> |
| 215 | </div> |
| 216 | <div className="info-item"> |
| 217 | <h3 className="info-title">视频格式</h3> |
| 218 | <p className="info-desc">mp4/mov</p> |
| 219 | </div> |
| 220 | <div className="info-item"> |
| 221 | <h3 className="info-title">分辨率</h3> |
| 222 | <p className="info-desc">建议720P及以上</p> |
| 223 | </div> |
| 224 | </> |
| 225 | )} |
| 226 | </div> |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 227 | </div> |
wu | eb6e6ca | 2025-06-15 10:35:32 +0800 | [diff] [blame] | 228 | ) |
wu | a80b90d | 2025-06-15 10:36:02 +0800 | [diff] [blame] | 229 | } |