/* 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 (
管理所有上传的文件
| 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()} |
|
确定要删除选中的 {selectedFiles.length} 个文件吗?此操作不可恢复。
)} {batchAction === 'download' && (确定要下载选中的 {selectedFiles.length} 个文件吗?
)}