blob: dbbd7fef2f5c561a4f6862c7d49a4a3df184decd [file] [log] [blame]
wua80b90d2025-06-15 10:36:02 +08001// src/components/UploadPage.jsx
2
wueb6e6ca2025-06-15 10:35:32 +08003import React, { useState } from 'react'
4import { Image, Video } from 'lucide-react'
5
6
wua80b90d2025-06-15 10:36:02 +08007/**
8 * @param {Object} props
9 * @param {(files: File[]) => void} [props.onComplete] 上传完成后回调,接收 File 数组
10 */
11export default function UploadPage({ onComplete }) {
wueb6e6ca2025-06-15 10:35:32 +080012 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']
wua80b90d2025-06-15 10:36:02 +080021 const types = activeTab === 'video' ? vidTypes : imgTypes
22 const max = activeTab === 'video'
23 ? 2 * 1024 * 1024 * 1024
24 : 32 * 1024 * 1024
wueb6e6ca2025-06-15 10:35:32 +080025
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)
wua80b90d2025-06-15 10:36:02 +080038
wueb6e6ca2025-06-15 10:35:32 +080039 const iv = setInterval(() => {
40 setUploadProgress(p => {
41 if (p >= 100) {
42 clearInterval(iv)
43 setIsUploading(false)
44 alert(`成功上传了 ${files.length} 个文件`)
wua80b90d2025-06-15 10:36:02 +080045 // 上传完成后回调
46 if (typeof onComplete === 'function') {
47 onComplete(files)
48 }
wueb6e6ca2025-06-15 10:35:32 +080049 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')
wua80b90d2025-06-15 10:36:02 +080059 input.type = 'file'
60 input.accept = activeTab === 'video' ? 'video/*' : 'image/*'
61 input.multiple = activeTab === 'image'
wueb6e6ca2025-06-15 10:35:32 +080062 input.onchange = e => {
63 const files = Array.from(e.target.files)
wua80b90d2025-06-15 10:36:02 +080064 if (files.length > 0 && validateFiles(files)) {
65 simulateUpload(files)
66 }
wueb6e6ca2025-06-15 10:35:32 +080067 }
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)
wua80b90d2025-06-15 10:36:02 +080077 if (files.length > 0 && validateFiles(files)) {
78 simulateUpload(files)
79 }
wueb6e6ca2025-06-15 10:35:32 +080080 }
81
82 const clearFiles = () => setUploadedFiles([])
wua80b90d2025-06-15 10:36:02 +080083 const removeFile = idx => setUploadedFiles(prev => prev.filter((_, i) => i !== idx))
wueb6e6ca2025-06-15 10:35:32 +080084
85 return (
wua80b90d2025-06-15 10:36:02 +080086 <div className="upload-page">
87 {/* 上传类型切换 */}
wueb6e6ca2025-06-15 10:35:32 +080088 <div className="upload-tabs">
89 <button
wua80b90d2025-06-15 10:36:02 +080090 className={`upload-tab${activeTab === 'video' ? ' active' : ''}`}
wueb6e6ca2025-06-15 10:35:32 +080091 onClick={() => setActiveTab('video')}
wua80b90d2025-06-15 10:36:02 +080092 >
93 上传视频
94 </button>
wueb6e6ca2025-06-15 10:35:32 +080095 <button
wua80b90d2025-06-15 10:36:02 +080096 className={`upload-tab${activeTab === 'image' ? ' active' : ''}`}
wueb6e6ca2025-06-15 10:35:32 +080097 onClick={() => setActiveTab('image')}
wua80b90d2025-06-15 10:36:02 +080098 >
99 上传图文
100 </button>
wueb6e6ca2025-06-15 10:35:32 +0800101 </div>
102
wua80b90d2025-06-15 10:36:02 +0800103 {/* 拖拽/点击上传区域 */}
wueb6e6ca2025-06-15 10:35:32 +0800104 <div
wua80b90d2025-06-15 10:36:02 +0800105 className={`upload-area${isDragOver ? ' drag-over' : ''}`}
wueb6e6ca2025-06-15 10:35:32 +0800106 onDragOver={handleDragOver}
107 onDragLeave={handleDragLeave}
108 onDrop={handleDrop}
109 >
110 <div className="upload-icon">
wua80b90d2025-06-15 10:36:02 +0800111 {activeTab === 'video' ? <Video size={48} /> : <Image size={48} />}
wueb6e6ca2025-06-15 10:35:32 +0800112 </div>
113 <h2 className="upload-title">
wua80b90d2025-06-15 10:36:02 +0800114 {activeTab === 'video'
wueb6e6ca2025-06-15 10:35:32 +0800115 ? '拖拽视频到此处或点击上传'
wua80b90d2025-06-15 10:36:02 +0800116 : '拖拽图片到此处或点击上传'}
wueb6e6ca2025-06-15 10:35:32 +0800117 </h2>
118 <p className="upload-subtitle">(需支持上传格式)</p>
119 <button
wua80b90d2025-06-15 10:36:02 +0800120 className={`upload-btn${isUploading ? ' uploading' : ''}`}
wueb6e6ca2025-06-15 10:35:32 +0800121 onClick={handleFileUpload}
122 disabled={isUploading}
123 >
124 {isUploading
125 ? `上传中... ${uploadProgress}%`
wua80b90d2025-06-15 10:36:02 +0800126 : activeTab === 'video'
wueb6e6ca2025-06-15 10:35:32 +0800127 ? '上传视频'
wua80b90d2025-06-15 10:36:02 +0800128 : '上传图片'}
wueb6e6ca2025-06-15 10:35:32 +0800129 </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
wua80b90d2025-06-15 10:36:02 +0800144 {/* 已上传文件预览 */}
wueb6e6ca2025-06-15 10:35:32 +0800145 {uploadedFiles.length > 0 && (
146 <div className="file-preview-area">
147 <div className="preview-header">
wua80b90d2025-06-15 10:36:02 +0800148 <h3 className="preview-title">
149 已上传文件 ({uploadedFiles.length})
150 </h3>
151 <button
152 className="clear-files-btn"
153 onClick={clearFiles}
154 >
wueb6e6ca2025-06-15 10:35:32 +0800155 清除所有
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="删除文件"
wua80b90d2025-06-15 10:36:02 +0800165 >
166 ×
167 </button>
wueb6e6ca2025-06-15 10:35:32 +0800168 {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
wua80b90d2025-06-15 10:36:02 +0800180 ? file.name.slice(0, 17) + '...'
181 : file.name}
wueb6e6ca2025-06-15 10:35:32 +0800182 </div>
183 <div className="file-size">
wua80b90d2025-06-15 10:36:02 +0800184 {(file.size / 1024 / 1024).toFixed(2)} MB
wueb6e6ca2025-06-15 10:35:32 +0800185 </div>
186 </div>
187 </div>
188 ))}
189 </div>
190 </div>
191 )}
192
wua80b90d2025-06-15 10:36:02 +0800193 {/* 上传说明信息 */}
wueb6e6ca2025-06-15 10:35:32 +0800194 <div className="upload-info fade-in">
wua80b90d2025-06-15 10:36:02 +0800195 {activeTab === 'image' ? (
wueb6e6ca2025-06-15 10:35:32 +0800196 <>
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>
wua80b90d2025-06-15 10:36:02 +0800227 </div>
wueb6e6ca2025-06-15 10:35:32 +0800228 )
wua80b90d2025-06-15 10:36:02 +0800229}