518 lines
20 KiB
JavaScript
518 lines
20 KiB
JavaScript
|
|
/* 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 (
|
|||
|
|
<div className="d-flex justify-content-center align-items-center vh-100">
|
|||
|
|
<div className="spinner-border text-primary" role="status">
|
|||
|
|
<span className="visually-hidden">Loading...</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<>
|
|||
|
|
<div className="admin-container">
|
|||
|
|
<div className="admin-content">
|
|||
|
|
{/* 头部 */}
|
|||
|
|
<div className="file-management-header">
|
|||
|
|
<h2 className="file-management-title">文件管理</h2>
|
|||
|
|
<p className="file-management-subtitle">管理所有上传的文件</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 筛选栏 */}
|
|||
|
|
<div className="file-management-filters">
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
className="search-input"
|
|||
|
|
placeholder="搜索文件名或描述..."
|
|||
|
|
value={searchQuery}
|
|||
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|||
|
|
/>
|
|||
|
|
<select
|
|||
|
|
className="filter-select"
|
|||
|
|
value={selectedCategory}
|
|||
|
|
onChange={(e) => setSelectedCategory(e.target.value)}
|
|||
|
|
>
|
|||
|
|
<option value="all">所有分类</option>
|
|||
|
|
{categories.map(cat => (
|
|||
|
|
<option key={cat.id} value={cat.id}>
|
|||
|
|
{cat.level > 0 ? ' '.repeat(cat.level) + '└ ' : ''}{cat.name}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
<select
|
|||
|
|
className="filter-select"
|
|||
|
|
value={filterStatus}
|
|||
|
|
onChange={(e) => setFilterStatus(e.target.value)}
|
|||
|
|
>
|
|||
|
|
<option value="all">所有状态</option>
|
|||
|
|
<option value="available">可领取</option>
|
|||
|
|
<option value="taken">已领取</option>
|
|||
|
|
</select>
|
|||
|
|
<select
|
|||
|
|
className="filter-select"
|
|||
|
|
value={sortBy}
|
|||
|
|
onChange={(e) => setSortBy(e.target.value)}
|
|||
|
|
>
|
|||
|
|
<option value="created_at">按上传时间排序</option>
|
|||
|
|
<option value="file_size">按文件大小排序</option>
|
|||
|
|
</select>
|
|||
|
|
<select
|
|||
|
|
className="filter-select"
|
|||
|
|
value={sortOrder}
|
|||
|
|
onChange={(e) => setSortOrder(e.target.value)}
|
|||
|
|
>
|
|||
|
|
<option value="desc">降序</option>
|
|||
|
|
<option value="asc">升序</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 批量操作条 */}
|
|||
|
|
{selectedFiles.length > 0 && (
|
|||
|
|
<div className="batch-operation-bar">
|
|||
|
|
<span className="batch-info">已选择 {selectedFiles.length} 个文件</span>
|
|||
|
|
<div className="batch-actions">
|
|||
|
|
<button className="btn-batch btn-batch-primary" onClick={() => handleBatchAction('download')}>
|
|||
|
|
批量下载
|
|||
|
|
</button>
|
|||
|
|
<button className="btn-batch btn-batch-danger" onClick={() => handleBatchAction('delete')}>
|
|||
|
|
批量删除
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 列表 */}
|
|||
|
|
<div className="file-list-container">
|
|||
|
|
<table className="file-table">
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th className="checkbox-column">
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={files.length > 0 && selectedFiles.length === files.length}
|
|||
|
|
onChange={handleSelectAll}
|
|||
|
|
disabled={files.length === 0}
|
|||
|
|
/>
|
|||
|
|
</th>
|
|||
|
|
<th className="id-column">ID</th>
|
|||
|
|
<th className="filename-column">文件名</th>
|
|||
|
|
<th className="size-column">大小</th>
|
|||
|
|
<th className="status-column">状态</th>
|
|||
|
|
<th className="taken-column">领取人</th>
|
|||
|
|
<th className="time-column">上传时间</th>
|
|||
|
|
<th className="actions-column">操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
{files.length === 0 ? (
|
|||
|
|
<tr>
|
|||
|
|
<td colSpan="8" className="empty-state">
|
|||
|
|
<div className="empty-content">
|
|||
|
|
<i className="empty-icon">📁</i>
|
|||
|
|
<p>暂无文件数据</p>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
) : (
|
|||
|
|
files.map(file => (
|
|||
|
|
<tr key={file.id} className="file-item">
|
|||
|
|
<td className="checkbox-column">
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={selectedFiles.includes(file.id)}
|
|||
|
|
onChange={() => handleSelectFile(file.id)}
|
|||
|
|
/>
|
|||
|
|
</td>
|
|||
|
|
<td className="id-column">{file.id}</td>
|
|||
|
|
<td className="filename-column">
|
|||
|
|
<div className="file-info">
|
|||
|
|
<span className="filename">{file.original_filename}</span>
|
|||
|
|
{file.description && (
|
|||
|
|
<span className="file-description">{file.description}</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td className="size-column">{formatFileSize(file.file_size)}</td>
|
|||
|
|
<td className="status-column">
|
|||
|
|
<span className={`status-badge ${file.is_taken ? 'status-taken' : 'status-available'}`}>
|
|||
|
|
{file.is_taken ? '已领取' : '可领取'}
|
|||
|
|
</span>
|
|||
|
|
</td>
|
|||
|
|
<td className="taken-column">
|
|||
|
|
{file.taken_by ? file.taken_by_username || `用户#${file.taken_by}` : '未领取'}
|
|||
|
|
</td>
|
|||
|
|
<td className="time-column">{new Date(file.created_at).toLocaleString()}</td>
|
|||
|
|
<td className="actions-column">
|
|||
|
|
<div className="file-actions">
|
|||
|
|
<button
|
|||
|
|
className="btn-action btn-download"
|
|||
|
|
onClick={() => downloadFile(file.id)}
|
|||
|
|
title="下载文件"
|
|||
|
|
>
|
|||
|
|
<i className="icon-download">↓</i>
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
className="btn-action btn-edit"
|
|||
|
|
onClick={() => openEditModal(file)}
|
|||
|
|
title="编辑文件"
|
|||
|
|
>
|
|||
|
|
<i className="icon-edit">✏️</i>
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
className="btn-action btn-delete"
|
|||
|
|
onClick={() => deleteFile(file.id)}
|
|||
|
|
title="删除文件"
|
|||
|
|
>
|
|||
|
|
<i className="icon-delete">🗑️</i>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
))
|
|||
|
|
)}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 分页 */}
|
|||
|
|
<div className="pagination-container">
|
|||
|
|
<div className="pagination-info">
|
|||
|
|
<span>共 <strong>{totalItems}</strong> 个文件</span>
|
|||
|
|
<span>第 <strong>{page}</strong> / {totalPages} 页</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="pagination-controls">
|
|||
|
|
<button
|
|||
|
|
className={`pagination-btn ${page === 1 ? 'disabled' : ''}`}
|
|||
|
|
onClick={() => setPage(page - 1)}
|
|||
|
|
disabled={page === 1}
|
|||
|
|
title="上一页"
|
|||
|
|
>
|
|||
|
|
← 上一页
|
|||
|
|
</button>
|
|||
|
|
<div className="pagination-numbers">
|
|||
|
|
{[...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 (
|
|||
|
|
<button
|
|||
|
|
key={index}
|
|||
|
|
className={`pagination-number ${isCurrent ? 'active' : ''}`}
|
|||
|
|
onClick={() => setPage(pageNum)}
|
|||
|
|
title={`第 ${pageNum} 页`}
|
|||
|
|
>
|
|||
|
|
{pageNum}
|
|||
|
|
</button>
|
|||
|
|
);
|
|||
|
|
} else if (pageNum === page - 3 || pageNum === page + 3) {
|
|||
|
|
return <span key={index} className="pagination-ellipsis">...</span>;
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
<button
|
|||
|
|
className={`pagination-btn ${page === totalPages ? 'disabled' : ''}`}
|
|||
|
|
onClick={() => setPage(page + 1)}
|
|||
|
|
disabled={page === totalPages}
|
|||
|
|
title="下一页"
|
|||
|
|
>
|
|||
|
|
下一页 →
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* ===== 以下弹窗与 admin-container 同级,保证始终居中视口 ===== */}
|
|||
|
|
{/* 编辑单文件弹窗 */}
|
|||
|
|
{showEditModal && (
|
|||
|
|
<div className="modal-overlay">
|
|||
|
|
<div className="admin-modal">
|
|||
|
|
<div className="modal-header">
|
|||
|
|
<h5 className="modal-title">编辑文件</h5>
|
|||
|
|
<button type="button" className="modal-close" onClick={() => setShowEditModal(false)}>×</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="modal-body">
|
|||
|
|
<div className="mb-3">
|
|||
|
|
<label className="form-label">描述</label>
|
|||
|
|
<textarea
|
|||
|
|
className="form-control"
|
|||
|
|
rows="3"
|
|||
|
|
value={description}
|
|||
|
|
onChange={(e) => setDescription(e.target.value)}
|
|||
|
|
></textarea>
|
|||
|
|
</div>
|
|||
|
|
<div className="mb-3">
|
|||
|
|
<label className="form-label">分类 (可选)</label>
|
|||
|
|
<select
|
|||
|
|
className="form-select"
|
|||
|
|
value={editFileCategoryId}
|
|||
|
|
onChange={(e) => setEditFileCategoryId(e.target.value)}
|
|||
|
|
>
|
|||
|
|
<option value="">无</option>
|
|||
|
|
{categories.map(cat => (
|
|||
|
|
<option key={cat.id} value={cat.id}>
|
|||
|
|
{cat.level > 0 ? ' '.repeat(cat.level) + '└ ' : ''}{cat.name}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="modal-footer">
|
|||
|
|
<button className="btn btn-secondary" onClick={() => setShowEditModal(false)}>取消</button>
|
|||
|
|
<button className="btn btn-primary" onClick={saveFileEdit}>保存</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 批量操作确认弹窗 */}
|
|||
|
|
{showBatchModal && (
|
|||
|
|
<div className="modal-overlay">
|
|||
|
|
<div className="admin-modal">
|
|||
|
|
<div className="modal-header">
|
|||
|
|
<h5 className="modal-title">确认批量操作</h5>
|
|||
|
|
<button type="button" className="modal-close" onClick={() => setShowBatchModal(false)}>×</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="modal-body">
|
|||
|
|
{batchAction === 'delete' && (
|
|||
|
|
<p>确定要删除选中的 {selectedFiles.length} 个文件吗?此操作不可恢复。</p>
|
|||
|
|
)}
|
|||
|
|
{batchAction === 'download' && (
|
|||
|
|
<p>确定要下载选中的 {selectedFiles.length} 个文件吗?</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div className="modal-footer">
|
|||
|
|
<button className="btn btn-secondary" onClick={() => setShowBatchModal(false)}>取消</button>
|
|||
|
|
<button className="btn btn-primary" onClick={executeBatchAction}>确认</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default FileManagement;
|