filesend/backend/app/uploads/42026ec5bfd24d5fbdb92f1cd6fad66b_FileUpload.js

399 lines
13 KiB
JavaScript
Raw Normal View History

2025-10-10 17:25:29 +08:00
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 (
<div style={{ paddingBottom: '60px' }}>
<h1 className="mb-4">上传文件</h1>
<div className="mb-3">
<Button
variant={uploadMode === 'single' ? 'primary' : 'outline-primary'}
onClick={() => setUploadMode('single')}
className="me-2"
>
单文件上传
</Button>
<Button
variant={uploadMode === 'batch' ? 'primary' : 'outline-primary'}
onClick={() => setUploadMode('batch')}
>
批量上传
</Button>
</div>
<Card>
<Card.Body>
<Form onSubmit={handleUpload}>
<Form.Group className="mb-3">
<Form.Label>
{uploadMode === 'single' ? '选择文件' : '选择多个文件'}
</Form.Label>
<Form.Control
type="file"
id="fileInput"
onChange={handleFileChange}
disabled={uploading}
multiple={uploadMode === 'batch'}
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>选择分类 <span className="text-danger">*</span></Form.Label>
<Form.Select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
disabled={uploading}
required
>
<option value="">请选择分类</option>
{categories.map(cat => (
<option key={cat.id} value={cat.id}>
{/* 使用缩进表示层级关系 */}
{cat.level > 0 ? ' '.repeat(cat.level) + '└ ' : ''}{cat.name}
</option>
))}
</Form.Select>
<Form.Text className="text-muted">
必须选择一个文件分类包括子分类
</Form.Text>
</Form.Group>
{selectedFiles.length > 0 && (
<div className="mb-3">
<h5>已选择的文件 ({selectedFiles.length}):</h5>
{selectedFiles.map((file, index) => (
<Card key={index} className="mb-2">
<Card.Body className="p-3">
<div className="d-flex justify-content-between align-items-start">
<div className="flex-grow-1">
<div className="d-flex align-items-center">
{filePreviews[file.name] && (
<img
src={filePreviews[file.name]}
alt="预览"
style={{ width: '50px', height: '50px', objectFit: 'cover', marginRight: '10px' }}
/>
)}
<div>
<strong>{file.name}</strong>
<div className="text-muted small">
{(file.size / 1024 / 1024).toFixed(2)} MB
</div>
</div>
</div>
<Form.Group className="mt-2">
<Form.Control
type="text"
placeholder="文件描述(可选)"
value={descriptions[file.name] || ''}
onChange={(e) => handleDescriptionChange(file.name, e.target.value)}
disabled={uploading}
size="sm"
/>
</Form.Group>
{progress[file.name] !== undefined && (
<ProgressBar
now={progress[file.name]}
label={`${progress[file.name]}%`}
className="mt-2"
/>
)}
</div>
<Button
variant="outline-danger"
size="sm"
onClick={() => removeFile(file.name)}
disabled={uploading}
className="ms-2"
>
移除
</Button>
</div>
</Card.Body>
</Card>
))}
</div>
)}
</Form>
</Card.Body>
</Card>
{/* 优化的底部上传按钮区域 */}
<div style={{
position: 'fixed',
bottom: '20px',
left: '50%',
transform: 'translateX(-50%)',
backgroundColor: 'white',
border: '1px solid #dee2e6',
borderRadius: '8px',
padding: '12px 20px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
zIndex: 1000,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '10px',
width: 'auto',
minWidth: '200px',
maxWidth: '90%'
}}>
<Button
type="button"
variant="primary"
onClick={handleUpload}
disabled={selectedFiles.length === 0 || uploading}
size="md"
>
{uploading ? '上传中...' :
uploadMode === 'single' ? '上传文件' : `批量上传 (${selectedFiles.length}个文件)`}
</Button>
{uploading && (
<Button
variant="outline-secondary"
onClick={resetForm}
size="md"
>
取消
</Button>
)}
</div>
</div>
);
};
export default FileUpload;