import React, { useState, useEffect } from 'react'; // 补充导入useEffect import axios from '../../services/axios'; import { toast } from 'react-toastify'; import { ProgressBar, Card, Button, Form } from 'react-bootstrap'; import { adminAPI } from '../../services/api'; const FileUpload = () => { const [selectedFiles, setSelectedFiles] = useState([]); const [descriptions, setDescriptions] = useState({}); const [uploading, setUploading] = useState(false); const [progress, setProgress] = useState({}); const [filePreviews, setFilePreviews] = useState({}); const [uploadMode, setUploadMode] = useState('single'); // 'single' or 'batch' const [categories, setCategories] = useState([]); const [selectedCategory, setSelectedCategory] = useState(''); useEffect(() => { const fetchCategories = async () => { try { // 添加hierarchical=true参数,获取层次化的分类数据 const res = await adminAPI.getCategories({hierarchical: true}); // 检查返回的数据结构 console.log('API返回的分类数据:', res.data); // 确保categories是一个数组 let categoriesData = []; if (res.data && res.data.categories) { // 如果API返回的是 {categories: [...]} 格式 categoriesData = res.data.categories || []; } else if (Array.isArray(res.data)) { // 如果API直接返回数组 categoriesData = res.data; } else { console.error('API返回的分类数据格式不正确:', res.data); categoriesData = []; } // 递归处理分类数据,确保每个分类都有必要的字段 const processCategories = (categories, level = 0) => { return categories.map(cat => ({ id: cat.id || cat._id || cat.category_id || '', name: cat.name || cat.category_name || '未命名分类', level: cat.level || level, parent_id: cat.parent_id || null, children: cat.children ? processCategories(cat.children, level + 1) : [] })); }; // 处理分类数据,构建扁平化的分类列表(用于下拉选择) const flattenCategories = (categories, result = []) => { categories.forEach(category => { result.push({ id: category.id, name: category.name, level: category.level, parent_id: category.parent_id }); if (category.children && category.children.length > 0) { flattenCategories(category.children, result); } }); return result; }; const processedCategories = processCategories(categoriesData); const flattenedCategories = flattenCategories(processedCategories); setCategories(flattenedCategories); } catch (err) { toast.error('获取分类列表失败'); console.error(err); setCategories([]); } }; fetchCategories(); }, []); const handleFileChange = (e) => { const files = Array.from(e.target.files); if (uploadMode === 'single') { if (files.length > 0) { const file = files[0]; setSelectedFiles([file]); // 生成文件预览(仅支持图片) if (file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (event) => { setFilePreviews({ [file.name]: event.target.result }); }; reader.readAsDataURL(file); } else { setFilePreviews({}); } } } else { // 批量上传模式 setSelectedFiles(files); files.forEach(file => { if (file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (event) => { setFilePreviews(prev => ({ ...prev, [file.name]: event.target.result })); }; reader.readAsDataURL(file); } }); } }; const handleUpload = async (e) => { e.preventDefault(); if (selectedFiles.length === 0) { toast.warning('请选择要上传的文件'); return; } if (!selectedCategory) { toast.warning('请选择文件分类'); return; } setUploading(true); if (uploadMode === 'single') { // 单文件上传 const file = selectedFiles[0]; const formData = new FormData(); formData.append('file', file); formData.append('description', descriptions[file.name] || ''); formData.append('category_id', selectedCategory); try { setProgress({ [file.name]: 0 }); await adminAPI.uploadFile(formData, (progressEvent) => { const percent = Math.round( (progressEvent.loaded / progressEvent.total) * 100 ); setProgress({ [file.name]: percent }); }); toast.success('文件上传成功!'); resetForm(); } catch (err) { toast.error(`文件上传失败: ${err.response?.data?.message || '请重试'}`); console.error(err); } finally { setUploading(false); } } else { // 批量上传(修复逻辑结构) const CONCURRENT_LIMIT = 3; const uploadQueue = [...selectedFiles]; const results = []; // 并发上传工作函数 const worker = async () => { while (uploadQueue.length > 0) { const file = uploadQueue.shift(); try { const formData = new FormData(); formData.append('file', file); formData.append('description', descriptions[file.name] || ''); formData.append('category_id', selectedCategory); await adminAPI.uploadFile(formData, (progressEvent) => { const percent = Math.round( (progressEvent.loaded / progressEvent.total) * 100 ); setProgress(prev => ({ ...prev, [file.name]: percent })); }); results.push({ file: file.name, status: 'success' }); } catch (error) { results.push({ file: file.name, status: 'error', error: error.message }); } } }; // 启动并发上传 const workers = Array(CONCURRENT_LIMIT).fill(null).map(worker); await Promise.allSettled(workers); // 统计结果 const successful = results.filter(r => r.status === 'success').length; const failed = results.filter(r => r.status === 'error').length; if (failed === 0) { toast.success(`批量上传完成!成功上传 ${successful} 个文件`); } else { toast.warning(`批量上传完成!成功 ${successful} 个,失败 ${failed} 个`); } resetForm(); setUploading(false); } }; const resetForm = () => { setSelectedFiles([]); setDescriptions({}); setProgress({}); setFilePreviews({}); setSelectedCategory(''); document.getElementById('fileInput').value = ''; }; const handleDescriptionChange = (filename, value) => { setDescriptions(prev => ({ ...prev, [filename]: value })); }; const removeFile = (filename) => { setSelectedFiles(prev => prev.filter(file => file.name !== filename)); setDescriptions(prev => { const newDescriptions = { ...prev }; delete newDescriptions[filename]; return newDescriptions; }); setProgress(prev => { const newProgress = { ...prev }; delete newProgress[filename]; return newProgress; }); setFilePreviews(prev => { const newPreviews = { ...prev }; delete newPreviews[filename]; return newPreviews; }); }; return (

上传文件

{uploadMode === 'single' ? '选择文件' : '选择多个文件'} 选择分类 * setSelectedCategory(e.target.value)} disabled={uploading} required > {categories.map(cat => ( ))} 必须选择一个文件分类(包括子分类) {selectedFiles.length > 0 && (
已选择的文件 ({selectedFiles.length}个):
{selectedFiles.map((file, index) => (
{filePreviews[file.name] && ( 预览 )}
{file.name}
{(file.size / 1024 / 1024).toFixed(2)} MB
handleDescriptionChange(file.name, e.target.value)} disabled={uploading} size="sm" /> {progress[file.name] !== undefined && ( )}
))}
)}
{/* 固定在底部的上传按钮区域 */}
{uploading && ( )}
); }; export default FileUpload;