399 lines
13 KiB
JavaScript
399 lines
13 KiB
JavaScript
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; |