/* FileManagement.js – 直接复制即可使用 */ import React, { useState, useEffect, useCallback } from 'react'; import axios from '../../services/axios'; import { adminAPI } from '../../services/api'; import { toast } from 'react-toastify'; import Pagination from '../../components/Pagination'; import '../../styles/Pagination.css'; import '../../styles/file-management.css'; import { debounce } from '../../utils/debounce'; const FileManagement = () => { const [files, setFiles] = useState([]); const [loading, setLoading] = useState(true); const [page, setPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(20); const [totalPages, setTotalPages] = useState(1); const [totalItems, setTotalItems] = useState(0); const [searchQuery, setSearchQuery] = useState(''); const [sortBy, setSortBy] = useState('created_at'); const [sortOrder, setSortOrder] = useState('desc'); const [showEditModal, setShowEditModal] = useState(false); const [currentFile, setCurrentFile] = useState(null); const [description, setDescription] = useState(''); const [filterStatus, setFilterStatus] = useState('all'); const [selectedFiles, setSelectedFiles] = useState([]); const [showBatchModal, setShowBatchModal] = useState(false); const [batchAction, setBatchAction] = useState(''); const [categories, setCategories] = useState([]); const [selectedCategory, setSelectedCategory] = useState('all'); const [editFileCategoryId, setEditFileCategoryId] = useState(''); /* ---------------- 数据获取 ---------------- */ const fetchCategories = async () => { try { const res = await adminAPI.getCategories({ hierarchical: true }); const data = res.data?.categories || res.data || []; const processCategories = (cats, level = 0) => { let result = []; cats.forEach(cat => { const item = { ...cat, id: cat.id || cat._id || cat.category_id || '', name: cat.name || cat.category_name || '未命名分类', level: cat.level || level, children: cat.children || [], }; result.push(item); if (item.children.length) { result = result.concat(processCategories(item.children, level + 1)); } }); return result; }; setCategories(processCategories(data)); } catch (err) { toast.error('获取分类列表失败'); console.error(err); setCategories([]); } }; const fetchFiles = useCallback(async (reset = false) => { try { setLoading(true); const currentPage = reset ? 1 : page; if (reset) setPage(1); const params = { page: currentPage, per_page: itemsPerPage, search: searchQuery, category_id: selectedCategory === 'all' ? '' : selectedCategory, sort_by: sortBy, sort_order: sortOrder, status: filterStatus === 'all' ? '' : filterStatus, }; const res = await axios.get('/api/admin/files', { params }); const { files: newFiles, total_pages, total_files } = res.data; setFiles(newFiles); setTotalPages(total_pages); setTotalItems(total_files); } catch (err) { toast.error('获取文件列表失败'); console.error(err); } finally { setLoading(false); } }, [page, itemsPerPage, searchQuery, selectedCategory, sortBy, sortOrder, filterStatus]); useEffect(() => { fetchCategories(); }, []); const debouncedSearch = useCallback(debounce(() => fetchFiles(true), 500), [fetchFiles]); useEffect(() => { debouncedSearch(); }, [searchQuery, debouncedSearch]); useEffect(() => { fetchFiles(); }, [filterStatus, selectedCategory, sortBy, sortOrder, itemsPerPage, page]); /* ---------------- 工具方法 ---------------- */ const formatFileSize = (bytes) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; /* ---------------- 文件操作 ---------------- */ const downloadFile = async (fileId) => { try { const res = await axios.get(`/api/admin/files/${fileId}/download`, { responseType: 'blob' }); const file = files.find(f => f.id === fileId); const url = window.URL.createObjectURL(new Blob([res.data])); const a = document.createElement('a'); a.href = url; a.download = file?.original_filename || 'file'; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (err) { toast.error('下载文件失败'); console.error(err); } }; const deleteFile = async (fileId) => { if (!window.confirm('确定要删除这个文件吗?此操作不可恢复。')) return; try { const res = await adminAPI.deleteFile(fileId); toast.success(res.data.message || '文件已删除'); fetchFiles(); } catch (err) { toast.error(err.response?.data?.message || '删除文件失败'); console.error(err); } }; const openEditModal = (file) => { setCurrentFile(file); setDescription(file.description || ''); setEditFileCategoryId(file.category_id || ''); setShowEditModal(true); }; const saveFileEdit = async () => { if (!currentFile) return; try { await axios.put(`/api/admin/files/${currentFile.id}`, { description, category_id: editFileCategoryId || null, }); toast.success('文件信息已更新'); setShowEditModal(false); fetchFiles(); } catch (err) { toast.error('更新文件信息失败'); console.error(err); } }; /* ---------------- 批量逻辑 ---------------- */ const handleSelectFile = (fileId) => { setSelectedFiles(prev => prev.includes(fileId) ? prev.filter(id => id !== fileId) : [...prev, fileId] ); }; const handleSelectAll = () => { if (selectedFiles.length === files.length) { setSelectedFiles([]); } else { setSelectedFiles(files.map(f => f.id)); } }; const handleBatchAction = (action) => { if (selectedFiles.length === 0) { toast.warning('请先选择文件'); return; } setBatchAction(action); setShowBatchModal(true); }; const executeBatchAction = async () => { let success = 0; try { if (batchAction === 'delete') { if (!window.confirm(`确定要删除选中的 ${selectedFiles.length} 个文件吗?此操作不可恢复。`)) return; for (const id of selectedFiles) { try { await adminAPI.deleteFile(id); success++; } catch (e) { console.error(`删除文件 ${id} 失败`, e); } } toast.success(`成功删除 ${success}/${selectedFiles.length} 个文件`); } else if (batchAction === 'download') { for (const id of selectedFiles) { try { await downloadFile(id); success++; } catch (e) { console.error(`下载文件 ${id} 失败`, e); } } toast.success(`开始下载 ${success}/${selectedFiles.length} 个文件`); } } catch (err) { toast.error('批量操作失败'); console.error(err); } finally { setShowBatchModal(false); setSelectedFiles([]); fetchFiles(); } }; /* ---------------- 渲染 ---------------- */ if (loading) { return (
Loading...
); } return ( <>
{/* 头部 */}

文件管理

管理所有上传的文件

{/* 筛选栏 */}
setSearchQuery(e.target.value)} />
{/* 批量操作条 */} {selectedFiles.length > 0 && (
已选择 {selectedFiles.length} 个文件
)} {/* 列表 */}
{files.length === 0 ? ( ) : ( files.map(file => ( )) )}
0 && selectedFiles.length === files.length} onChange={handleSelectAll} disabled={files.length === 0} /> ID 文件名 大小 状态 领取人 上传时间 操作
📁

暂无文件数据

handleSelectFile(file.id)} /> {file.id}
{file.original_filename} {file.description && ( {file.description} )}
{formatFileSize(file.file_size)} {file.is_taken ? '已领取' : '可领取'} {file.taken_by ? file.taken_by_username || `用户#${file.taken_by}` : '未领取'} {new Date(file.created_at).toLocaleString()}
{/* 分页 */}
{totalItems} 个文件 {page} / {totalPages} 页
{[...Array(totalPages)].map((_, index) => { const pageNum = index + 1; const isCurrent = page === pageNum; const isNearCurrent = Math.abs(pageNum - page) <= 2; const isFirstOrLast = pageNum === 1 || pageNum === totalPages; if (totalPages <= 7 || isFirstOrLast || isNearCurrent) { return ( ); } else if (pageNum === page - 3 || pageNum === page + 3) { return ...; } return null; })}
{/* ===== 以下弹窗与 admin-container 同级,保证始终居中视口 ===== */} {/* 编辑单文件弹窗 */} {showEditModal && (
编辑文件
)} {/* 批量操作确认弹窗 */} {showBatchModal && (
确认批量操作
{batchAction === 'delete' && (

确定要删除选中的 {selectedFiles.length} 个文件吗?此操作不可恢复。

)} {batchAction === 'download' && (

确定要下载选中的 {selectedFiles.length} 个文件吗?

)}
)} ); }; export default FileManagement;