956303669 | 80c1f27 | 2025-06-20 14:08:54 +0800 | [diff] [blame^] | 1 | import React, { useState } from 'react' |
| 2 | import { Image, Video, Send } from 'lucide-react' |
| 3 | import { searchAPI } from '../api/search_jwlll' |
| 4 | import '../style/UploadPage.css' |
| 5 | |
| 6 | const categories = [ |
| 7 | '穿搭','美食','彩妆','影视', |
| 8 | '职场','情感','家居','游戏','旅行','健身' |
| 9 | ] |
| 10 | |
| 11 | export default function UploadPageJWLLL({ onComplete }) { |
| 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 | // 新增表单字段 |
| 19 | const [title, setTitle] = useState('') |
| 20 | const [content, setContent] = useState('') |
| 21 | const [tags, setTags] = useState('') |
| 22 | const [category, setCategory] = useState(categories[0]) |
| 23 | const [isPublishing, setIsPublishing] = useState(false) |
| 24 | |
| 25 | const DEFAULT_USER_ID = '3' // 默认用户ID |
| 26 | |
| 27 | const validateFiles = files => { |
| 28 | const imgTypes = ['image/jpeg','image/jpg','image/png','image/webp'] |
| 29 | const vidTypes = ['video/mp4','video/mov','video/avi'] |
| 30 | const types = activeTab==='video'? vidTypes : imgTypes |
| 31 | const max = activeTab==='video'? 2*1024*1024*1024 : 32*1024*1024 |
| 32 | |
| 33 | const invalid = files.filter(f => !types.includes(f.type) || f.size > max) |
| 34 | if (invalid.length) { |
| 35 | alert(`发现 ${invalid.length} 个无效文件,请检查文件格式和大小`) |
| 36 | return false |
| 37 | } |
| 38 | return true |
| 39 | } |
| 40 | |
| 41 | const simulateUpload = files => { |
| 42 | setIsUploading(true) |
| 43 | setUploadProgress(0) |
| 44 | setUploadedFiles(files) |
| 45 | const iv = setInterval(() => { |
| 46 | setUploadProgress(p => { |
| 47 | if (p >= 100) { |
| 48 | clearInterval(iv) |
| 49 | setIsUploading(false) |
| 50 | if (typeof onComplete === 'function') { |
| 51 | onComplete(files) |
| 52 | } |
| 53 | return 100 |
| 54 | } |
| 55 | return p + 10 |
| 56 | }) |
| 57 | }, 200) |
| 58 | } |
| 59 | |
| 60 | const handleFileUpload = () => { |
| 61 | if (isUploading) return |
| 62 | const input = document.createElement('input') |
| 63 | input.type = 'file' |
| 64 | input.accept = activeTab==='video'? 'video/*' : 'image/*' |
| 65 | input.multiple = activeTab==='image' |
| 66 | input.onchange = e => { |
| 67 | const files = Array.from(e.target.files) |
| 68 | if (files.length > 0 && validateFiles(files)) simulateUpload(files) |
| 69 | } |
| 70 | input.click() |
| 71 | } |
| 72 | |
| 73 | const handleDragOver = e => { e.preventDefault(); e.stopPropagation(); setIsDragOver(true) } |
| 74 | const handleDragLeave = e => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false) } |
| 75 | const handleDrop = e => { |
| 76 | e.preventDefault(); e.stopPropagation(); setIsDragOver(false) |
| 77 | if (isUploading) return |
| 78 | const files = Array.from(e.dataTransfer.files) |
| 79 | if (files.length > 0 && validateFiles(files)) simulateUpload(files) |
| 80 | } |
| 81 | |
| 82 | const clearFiles = () => setUploadedFiles([]) |
| 83 | const removeFile = idx => setUploadedFiles(f => f.filter((_,i) => i!==idx)) |
| 84 | |
| 85 | // 发布帖子 |
| 86 | const handlePublish = async () => { |
| 87 | if (!title.trim()) { |
| 88 | alert('请输入标题') |
| 89 | return |
| 90 | } |
| 91 | if (!content.trim()) { |
| 92 | alert('请输入内容') |
| 93 | return |
| 94 | } |
| 95 | |
| 96 | setIsPublishing(true) |
| 97 | try { |
| 98 | const postData = { |
| 99 | user_id: DEFAULT_USER_ID, |
| 100 | title: title.trim(), |
| 101 | content: content.trim(), |
| 102 | tags: tags.split(',').map(t => t.trim()).filter(t => t), |
| 103 | category: category, |
| 104 | type: activeTab === 'video' ? 'video' : 'image', |
| 105 | media_files: uploadedFiles.map(f => f.name) // 实际项目中应该是上传后的URL |
| 106 | } |
| 107 | |
| 108 | await searchAPI.uploadPost(postData) |
| 109 | alert('发布成功!') |
| 110 | |
| 111 | // 清空表单 |
| 112 | setTitle('') |
| 113 | setContent('') |
| 114 | setTags('') |
| 115 | setUploadedFiles([]) |
| 116 | setActiveTab('image') |
| 117 | |
| 118 | } catch (error) { |
| 119 | console.error('发布失败:', error) |
| 120 | alert('发布失败,请重试') |
| 121 | } finally { |
| 122 | setIsPublishing(false) |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | return ( |
| 127 | <div className="upload-page-jwlll"> |
| 128 | <div className="upload-tabs"> |
| 129 | <button |
| 130 | className={`upload-tab${activeTab==='video'?' active':''}`} |
| 131 | onClick={() => setActiveTab('video')} |
| 132 | >上传视频</button> |
| 133 | <button |
| 134 | className={`upload-tab${activeTab==='image'?' active':''}`} |
| 135 | onClick={() => setActiveTab('image')} |
| 136 | >上传图文</button> |
| 137 | </div> |
| 138 | |
| 139 | {/* 内容表单 */} |
| 140 | <div className="content-form"> |
| 141 | <div className="form-group"> |
| 142 | <label htmlFor="title">标题</label> |
| 143 | <input |
| 144 | id="title" |
| 145 | type="text" |
| 146 | value={title} |
| 147 | onChange={(e) => setTitle(e.target.value)} |
| 148 | placeholder="请输入标题..." |
| 149 | className="form-input" |
| 150 | maxLength={100} |
| 151 | /> |
| 152 | </div> |
| 153 | |
| 154 | <div className="form-group"> |
| 155 | <label htmlFor="content">内容</label> |
| 156 | <textarea |
| 157 | id="content" |
| 158 | value={content} |
| 159 | onChange={(e) => setContent(e.target.value)} |
| 160 | placeholder="请输入内容..." |
| 161 | className="form-textarea" |
| 162 | rows={4} |
| 163 | maxLength={1000} |
| 164 | /> |
| 165 | </div> |
| 166 | |
| 167 | <div className="form-row"> |
| 168 | <div className="form-group"> |
| 169 | <label htmlFor="category">分类</label> |
| 170 | <select |
| 171 | id="category" |
| 172 | value={category} |
| 173 | onChange={(e) => setCategory(e.target.value)} |
| 174 | className="form-select" |
| 175 | > |
| 176 | {categories.map(cat => ( |
| 177 | <option key={cat} value={cat}>{cat}</option> |
| 178 | ))} |
| 179 | </select> |
| 180 | </div> |
| 181 | |
| 182 | <div className="form-group"> |
| 183 | <label htmlFor="tags">标签</label> |
| 184 | <input |
| 185 | id="tags" |
| 186 | type="text" |
| 187 | value={tags} |
| 188 | onChange={(e) => setTags(e.target.value)} |
| 189 | placeholder="用逗号分隔多个标签..." |
| 190 | className="form-input" |
| 191 | /> |
| 192 | </div> |
| 193 | </div> |
| 194 | </div> |
| 195 | |
| 196 | {/* 文件上传区域 */} |
| 197 | <div |
| 198 | className={`upload-area${isDragOver?' drag-over':''}`} |
| 199 | onDragOver={handleDragOver} |
| 200 | onDragLeave={handleDragLeave} |
| 201 | onDrop={handleDrop} |
| 202 | > |
| 203 | <div className="upload-icon"> |
| 204 | {activeTab==='video'? <Video/> : <Image/>} |
| 205 | </div> |
| 206 | <h2 className="upload-title"> |
| 207 | {activeTab==='video' |
| 208 | ? '拖拽视频到此处或点击上传' |
| 209 | : '拖拽图片到此处或点击上传' |
| 210 | } |
| 211 | </h2> |
| 212 | <p className="upload-subtitle">(需支持上传格式)</p> |
| 213 | <button |
| 214 | className={`upload-btn${isUploading?' uploading':''}`} |
| 215 | onClick={handleFileUpload} |
| 216 | disabled={isUploading} |
| 217 | > |
| 218 | {isUploading |
| 219 | ? `上传中... ${uploadProgress}%` |
| 220 | : activeTab==='video' |
| 221 | ? '上传视频' |
| 222 | : '上传图片' |
| 223 | } |
| 224 | </button> |
| 225 | |
| 226 | {isUploading && ( |
| 227 | <div className="progress-container"> |
| 228 | <div className="progress-bar"> |
| 229 | <div |
| 230 | className="progress-fill" |
| 231 | style={{ width: `${uploadProgress}%` }} |
| 232 | /> |
| 233 | </div> |
| 234 | <div className="progress-text">{uploadProgress}%</div> |
| 235 | </div> |
| 236 | )} |
| 237 | </div> |
| 238 | |
| 239 | {uploadedFiles.length > 0 && ( |
| 240 | <div className="file-preview-area"> |
| 241 | <div className="preview-header"> |
| 242 | <h3 className="preview-title">已上传文件 ({uploadedFiles.length})</h3> |
| 243 | <button className="clear-files-btn" onClick={clearFiles}> |
| 244 | 清除所有 |
| 245 | </button> |
| 246 | </div> |
| 247 | <div className="file-grid"> |
| 248 | {uploadedFiles.map((file, i) => ( |
| 249 | <div key={i} className="file-item"> |
| 250 | <button |
| 251 | className="remove-file-btn" |
| 252 | onClick={() => removeFile(i)} |
| 253 | title="删除文件" |
| 254 | >×</button> |
| 255 | {file.type.startsWith('image/') ? ( |
| 256 | <div className="file-thumbnail"> |
| 257 | <img src={URL.createObjectURL(file)} alt={file.name} /> |
| 258 | </div> |
| 259 | ) : ( |
| 260 | <div className="file-thumbnail video-thumbnail"> |
| 261 | <Video size={24} /> |
| 262 | </div> |
| 263 | )} |
| 264 | <div className="file-info"> |
| 265 | <div className="file-name" title={file.name}> |
| 266 | {file.name.length > 20 |
| 267 | ? file.name.slice(0,17) + '...' |
| 268 | : file.name |
| 269 | } |
| 270 | </div> |
| 271 | <div className="file-size"> |
| 272 | {(file.size/1024/1024).toFixed(2)} MB |
| 273 | </div> |
| 274 | </div> |
| 275 | </div> |
| 276 | ))} |
| 277 | </div> |
| 278 | </div> |
| 279 | )} |
| 280 | |
| 281 | {/* 发布按钮 */} |
| 282 | <div className="publish-section"> |
| 283 | <button |
| 284 | className={`publish-btn${isPublishing?' publishing':''}`} |
| 285 | onClick={handlePublish} |
| 286 | disabled={isPublishing || !title.trim() || !content.trim()} |
| 287 | > |
| 288 | <Send size={20} /> |
| 289 | {isPublishing ? '发布中...' : '发布'} |
| 290 | </button> |
| 291 | </div> |
| 292 | |
| 293 | <div className="upload-info fade-in"> |
| 294 | {activeTab==='image' ? ( |
| 295 | <> |
| 296 | <div className="info-item"> |
| 297 | <h3 className="info-title">图片大小</h3> |
| 298 | <p className="info-desc">最大32MB</p> |
| 299 | </div> |
| 300 | <div className="info-item"> |
| 301 | <h3 className="info-title">图片格式</h3> |
| 302 | <p className="info-desc">png/jpg/jpeg/webp</p> |
| 303 | </div> |
| 304 | <div className="info-item"> |
| 305 | <h3 className="info-title">分辨率</h3> |
| 306 | <p className="info-desc">建议720×960及以上</p> |
| 307 | </div> |
| 308 | </> |
| 309 | ) : ( |
| 310 | <> |
| 311 | <div className="info-item"> |
| 312 | <h3 className="info-title">视频大小</h3> |
| 313 | <p className="info-desc">最大2GB,时长≤5分钟</p> |
| 314 | </div> |
| 315 | <div className="info-item"> |
| 316 | <h3 className="info-title">视频格式</h3> |
| 317 | <p className="info-desc">mp4/mov</p> |
| 318 | </div> |
| 319 | <div className="info-item"> |
| 320 | <h3 className="info-title">分辨率</h3> |
| 321 | <p className="info-desc">建议720P及以上</p> |
| 322 | </div> |
| 323 | </> |
| 324 | )} |
| 325 | </div> |
| 326 | </div> |
| 327 | ) |
| 328 | } |