From b4d71c7d80992280846e64d0906098ff6c06e57b Mon Sep 17 00:00:00 2001 From: MoeexT Date: Fri, 13 Mar 2026 11:13:55 +0800 Subject: [PATCH 1/3] fix: file count error after deletion --- .../DatasetFileApplicationService.java | 89 ++++++++++++------- .../persistence/mapper/DatasetMapper.java | 7 ++ .../repository/DatasetRepository.java | 5 ++ .../impl/DatasetRepositoryImpl.java | 5 ++ .../DataAnnotation/Home/DataAnnotation.tsx | 20 +++++ .../DataManagement/Detail/DatasetDetail.tsx | 13 +-- .../Detail/useFilesOperation.ts | 29 ++++-- 7 files changed, 125 insertions(+), 43 deletions(-) diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/DatasetFileApplicationService.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/DatasetFileApplicationService.java index 05f2cbe7c..21936376d 100644 --- a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/DatasetFileApplicationService.java +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/DatasetFileApplicationService.java @@ -226,34 +226,50 @@ public DatasetFile getDatasetFile(Dataset dataset, String fileId, String prefix) /** * 删除文件 + * 使用悲观锁防止并发删除导致 fileCount 计算不准确 */ @Transactional public void deleteDatasetFile(String datasetId, String fileId, String prefix) { - Dataset dataset = datasetRepository.getById(datasetId); + // 使用悲观锁获取 dataset,确保并发安全 + Dataset dataset = datasetRepository.getByIdWithLock(datasetId); DatasetFile file = getDatasetFile(dataset, fileId, prefix); - dataset.setFiles(new ArrayList<>(Collections.singleton(file))); + + // 先删除数据库记录 datasetFileRepository.removeById(fileId); - if (CommonUtils.isUUID(fileId)) { - dataset.removeFile(file); - } - datasetRepository.updateById(dataset); - // 删除文件时,上传到数据集中的文件会同时删除数据库中的记录和文件系统中的文件,归集过来的文件仅删除数据库中的记录 + + // 删除物理文件(仅删除在数据集路径下的文件) if (file.getFilePath().startsWith(dataset.getPath())) { try { Path filePath = validateAndResolvePath(file.getFilePath(), dataset.getPath()); Files.deleteIfExists(filePath); } catch (IOException ex) { + log.error("删除物理文件失败: {}", file.getFilePath(), ex); throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR); } } + + // 重新计算文件统计信息(确保数据准确) + List remainingFiles = datasetFileRepository.findAllByDatasetId(datasetId); + dataset.setFileCount(remainingFiles.size()); + dataset.setSizeBytes(remainingFiles.stream() + .mapToLong(f -> f.getFileSize() != null ? f.getFileSize().longValue() : 0L) + .sum()); + dataset.setUpdatedAt(LocalDateTime.now()); + + datasetRepository.updateById(dataset); + + log.info("删除文件成功 - datasetId: {}, fileId: {}, fileName: {}, fileCount: {}, sizeBytes: {}", + datasetId, fileId, file.getFileName(), dataset.getFileCount(), dataset.getSizeBytes()); } /** * 批量删除文件 + * 使用悲观锁防止并发删除导致 fileCount 计算不准确 */ @Transactional public void batchDeleteFiles(String datasetId, BatchDeleteFilesRequest request) { - Dataset dataset = datasetRepository.getById(datasetId); + // 使用悲观锁获取 dataset,确保并发安全 + Dataset dataset = datasetRepository.getByIdWithLock(datasetId); if (dataset == null) { throw BusinessException.of(DataManagementErrorCode.DATASET_NOT_FOUND); } @@ -266,6 +282,7 @@ public void batchDeleteFiles(String datasetId, BatchDeleteFilesRequest request) List filesToDelete = new ArrayList<>(); List failedFileIds = new ArrayList<>(); + // 先删除数据库记录 for (String fileId : fileIds) { try { DatasetFile file = getDatasetFile(dataset, fileId, request.getPrefix()); @@ -277,20 +294,8 @@ public void batchDeleteFiles(String datasetId, BatchDeleteFilesRequest request) } } - // 更新数据集(避免 ConcurrentModificationException) - List datasetFiles = dataset.getFiles(); - if (datasetFiles != null) { - // 创建一个新的列表来存储要保留的文件 - List remainingFiles = new ArrayList<>(datasetFiles); - // 移除要删除的文件 - remainingFiles.removeAll(filesToDelete); - dataset.setFiles(remainingFiles); - } - datasetRepository.updateById(dataset); - - // 删除文件系统中的文件 + // 删除物理文件(仅删除在数据集路径下的文件) for (DatasetFile file : filesToDelete) { - // 上传到数据集中的文件会同时删除数据库中的记录和文件系统中的文件,归集过来的文件仅删除数据库中的记录 if (file.getFilePath().startsWith(dataset.getPath())) { try { Path filePath = validateAndResolvePath(file.getFilePath(), dataset.getPath()); @@ -303,6 +308,19 @@ public void batchDeleteFiles(String datasetId, BatchDeleteFilesRequest request) } } + // 重新计算文件统计信息(确保数据准确) + List remainingFiles = datasetFileRepository.findAllByDatasetId(datasetId); + dataset.setFileCount(remainingFiles.size()); + dataset.setSizeBytes(remainingFiles.stream() + .mapToLong(f -> f.getFileSize() != null ? f.getFileSize().longValue() : 0L) + .sum()); + dataset.setUpdatedAt(LocalDateTime.now()); + + datasetRepository.updateById(dataset); + + log.info("批量删除文件成功 - datasetId: {}, successCount: {}, failedCount: {}, fileCount: {}, sizeBytes: {}", + datasetId, filesToDelete.size(), failedFileIds.size(), dataset.getFileCount(), dataset.getSizeBytes()); + // 如果有失败的文件,记录日志但不抛出异常 if (!failedFileIds.isEmpty()) { log.warn("Failed to delete {} files out of {}", failedFileIds.size(), fileIds.size()); @@ -643,7 +661,8 @@ private void zipDirectory(Path sourceDir, Path basePath, ZipArchiveOutputStream */ @Transactional public void deleteDirectory(String datasetId, String prefix) { - Dataset dataset = datasetRepository.getById(datasetId); + // 使用悲观锁获取 dataset,防止并发删除导致 fileCount 不准确 + Dataset dataset = datasetRepository.getByIdWithLock(datasetId); if (dataset == null) { throw BusinessException.of(DataManagementErrorCode.DATASET_NOT_FOUND); } @@ -694,6 +713,10 @@ public void deleteDirectory(String datasetId, String prefix) { }) .collect(Collectors.toList()); + log.info("删除目录开始 - datasetId: {}, prefix: {}, fileCount: {}, filesToDelete: {}", + datasetId, prefix, dataset.getFileCount(), filesToDelete.size()); + + // 删除数据库记录 for (DatasetFile file : filesToDelete) { datasetFileRepository.removeById(file.getId()); } @@ -702,20 +725,22 @@ public void deleteDirectory(String datasetId, String prefix) { try { deleteDirectoryRecursively(normalized); } catch (IOException e) { - log.error("Failed to delete directory {} for dataset {}", normalized, datasetId, e); + log.error("删除目录失败: datasetId={}, prefix={}", datasetId, prefix, e); throw BusinessException.of(SystemErrorCode.FILE_SYSTEM_ERROR); } - // 更新数据集(避免 ConcurrentModificationException,先获取文件列表再删除) - List datasetFiles = dataset.getFiles(); - if (datasetFiles != null) { - // 创建一个新的列表来存储要保留的文件 - List remainingFiles = new ArrayList<>(datasetFiles); - // 移除要删除的文件 - remainingFiles.removeAll(filesToDelete); - dataset.setFiles(remainingFiles); - } + // 重新计算文件统计信息(确保数据准确) + List remainingFiles = datasetFileRepository.findAllByDatasetId(datasetId); + dataset.setFileCount(remainingFiles.size()); + dataset.setSizeBytes(remainingFiles.stream() + .mapToLong(f -> f.getFileSize() != null ? f.getFileSize().longValue() : 0L) + .sum()); + dataset.setUpdatedAt(LocalDateTime.now()); + datasetRepository.updateById(dataset); + + log.info("删除目录成功 - datasetId: {}, prefix: {}, deletedCount: {}, fileCount: {}, sizeBytes: {}", + datasetId, prefix, filesToDelete.size(), dataset.getFileCount(), dataset.getSizeBytes()); } /** diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/mapper/DatasetMapper.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/mapper/DatasetMapper.java index 445051187..6da91ceac 100644 --- a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/mapper/DatasetMapper.java +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/mapper/DatasetMapper.java @@ -5,6 +5,7 @@ import com.datamate.datamanagement.interfaces.dto.AllDatasetStatisticsResponse; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import org.apache.ibatis.session.RowBounds; import java.util.List; @@ -12,6 +13,12 @@ @Mapper public interface DatasetMapper extends BaseMapper { Dataset findById(@Param("id") String id); + + /** + * 使用悲观锁查询数据集(FOR UPDATE) + */ + @Select("SELECT * FROM t_dm_datasets WHERE id = #{id} FOR UPDATE") + Dataset findByIdWithLock(@Param("id") String id); Dataset findByName(@Param("name") String name); List findByStatus(@Param("status") String status); List findByCreatedBy(@Param("createdBy") String createdBy, RowBounds rowBounds); diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/DatasetRepository.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/DatasetRepository.java index b25716124..8be0904f6 100644 --- a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/DatasetRepository.java +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/DatasetRepository.java @@ -19,6 +19,11 @@ public interface DatasetRepository extends IRepository { Dataset findByName(String name); + /** + * 使用悲观锁获取数据集(用于更新操作,防止并发冲突) + */ + Dataset getByIdWithLock(String id); + List findByCriteria(String type, String status, String keyword, List tagList, RowBounds bounds); long countByCriteria(String type, String status, String keyword, List tagList); diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/DatasetRepositoryImpl.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/DatasetRepositoryImpl.java index 8bb6b0174..c75bc39a1 100644 --- a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/DatasetRepositoryImpl.java +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/infrastructure/persistence/repository/impl/DatasetRepositoryImpl.java @@ -31,6 +31,11 @@ public Dataset findByName(String name) { return datasetMapper.selectOne(new LambdaQueryWrapper().eq(Dataset::getName, name)); } + @Override + public Dataset getByIdWithLock(String id) { + return datasetMapper.findByIdWithLock(id); + } + @Override public List findByCriteria(String type, String status, String keyword, List tagList, RowBounds bounds) { diff --git a/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx b/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx index 88dbbcf10..2d5035925 100644 --- a/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx +++ b/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from "react"; +import { useNavigate } from "react-router"; import { Card, Button, Table, message, Modal, Tabs, Tag, Progress, Tooltip, Dropdown } from "antd"; import { PlusOutlined, @@ -38,6 +39,7 @@ import { useTranslation } from "react-i18next"; export default function DataAnnotation() { const { t } = useTranslation(); + const navigate = useNavigate(); // return ; const [activeTab, setActiveTab] = useState("tasks"); const [viewMode, setViewMode] = useState<"list" | "card">("list"); @@ -564,6 +566,24 @@ export default function DataAnnotation() { dataIndex: "datasetName", key: "datasetName", width: 180, + render: (text: string, record: any) => { + if (!text || text === "-") return "-"; + const datasetId = record.datasetId || record.dataset_id; + if (!datasetId) return text; + + return ( + { + e.preventDefault(); + e.stopPropagation(); + navigate(`/data/management/detail/${datasetId}`); + }} + style={{ cursor: 'pointer', color: '#1890ff' }} + > + {text} + + ); + }, }, { title: t('dataAnnotation.home.columns.model'), diff --git a/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx b/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx index 82c89951a..ef0829fb1 100644 --- a/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx +++ b/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx @@ -40,7 +40,14 @@ export default function DatasetDetail() { const [showDetailDrawer, setShowDetailDrawer] = useState(false); const [dataset, setDataset] = useState({} as Dataset); - const filesOperation = useFilesOperation(dataset); + + // 定义 fetchDataset,必须在 useFilesOperation 之前定义 + const fetchDataset = async () => { + const { data } = await queryDatasetByIdUsingGet(id as unknown as number); + setDataset(mapDataset(data, t)); + }; + + const filesOperation = useFilesOperation(dataset, fetchDataset); const [showUploadDialog, setShowUploadDialog] = useState(false); const navigateItems = useMemo( @@ -54,10 +61,6 @@ export default function DatasetDetail() { ], [dataset, t] ); - const fetchDataset = async () => { - const { data } = await queryDatasetByIdUsingGet(id as unknown as number); - setDataset(mapDataset(data, t)); - }; useEffect(() => { fetchDataset(); diff --git a/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts b/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts index 08b60331b..158f65277 100644 --- a/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts +++ b/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts @@ -19,7 +19,7 @@ import { } from "../dataset.api"; import { useParams } from "react-router"; -export function useFilesOperation(dataset: Dataset) { +export function useFilesOperation(dataset: Dataset, onDatasetUpdate?: () => Promise) { const { message } = App.useApp(); const { id } = useParams(); // 获取动态路由参数 @@ -125,7 +125,7 @@ export function useFilesOperation(dataset: Dataset) { } }); - // 批量删除文件(一次性删除所有文件) + // 批量删除文件(一次性删除所有文件,后端使用悲观锁保证并发安全) if (fileIds.length > 0) { try { await batchDeleteFilesUsingDelete(dataset.id, { fileIds, prefix }); @@ -139,7 +139,7 @@ export function useFilesOperation(dataset: Dataset) { } } - // 并发删除目录(目录使用单独的递归删除接口) + // 删除目录(后端会递归删除所有内容) const deleteDirectory = async (dir: any) => { try { const dirPath = `${prefix}${dir.fileName}/`; @@ -153,11 +153,19 @@ export function useFilesOperation(dataset: Dataset) { await Promise.all(directories.map(deleteDirectory)); - // 刷新文件列表 + // 刷新文件列表和数据集信息 await fetchFiles(prefix, 1, pagination.pageSize); setSelectedFiles([]); setSelectedFilesMap(new Map()); + if (onDatasetUpdate) { + try { + await onDatasetUpdate(); + } catch (error) { + console.error("Failed to refresh dataset info after deletion:", error); + } + } + hide(); if (failCount === 0) { @@ -411,7 +419,6 @@ export function useFilesOperation(dataset: Dataset) { } }; - return { fileList, selectedFiles, @@ -462,11 +469,21 @@ export function useFilesOperation(dataset: Dataset) { }, handleDeleteDirectory: async (directoryPath: string, directoryName: string) => { try { - // 直接调用后端递归删除目录 API,后端会一次性删除整个目录树 + // 直接调用后端API删除目录(后端会递归删除所有内容) await deleteDirectoryUsingDelete(dataset.id, directoryPath); // 删除成功后刷新当前目录 const currentPrefix = pagination.prefix || ""; await fetchFiles(currentPrefix, 1, pagination.pageSize); + + // 刷新数据集信息(更新 fileCount 和 totalSize) + if (onDatasetUpdate) { + try { + await onDatasetUpdate(); + } catch (error) { + console.error("Failed to refresh dataset info after directory deletion:", error); + } + } + message.success({ content: `文件夹 ${directoryName} 已删除` }); } catch (error) { message.error({ content: `文件夹 ${directoryName} 删除失败` }); From 1b6792d690643e3f3030f1cb12a41a1808207b1d Mon Sep 17 00:00:00 2001 From: MoeexT Date: Fri, 13 Mar 2026 14:30:56 +0800 Subject: [PATCH 2/3] fix: delete dataset tooltip --- .../DatasetFileApplicationService.java | 6 +-- frontend/src/i18n/locales/en/common.json | 2 + frontend/src/i18n/locales/zh/common.json | 2 + .../DataManagement/Detail/DatasetDetail.tsx | 28 +++++++---- .../Detail/components/Overview.tsx | 49 ++++++++++++++++++- 5 files changed, 73 insertions(+), 14 deletions(-) diff --git a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/DatasetFileApplicationService.java b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/DatasetFileApplicationService.java index 21936376d..1253e6d7d 100644 --- a/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/DatasetFileApplicationService.java +++ b/backend/services/data-management-service/src/main/java/com/datamate/datamanagement/application/DatasetFileApplicationService.java @@ -250,7 +250,7 @@ public void deleteDatasetFile(String datasetId, String fileId, String prefix) { // 重新计算文件统计信息(确保数据准确) List remainingFiles = datasetFileRepository.findAllByDatasetId(datasetId); - dataset.setFileCount(remainingFiles.size()); + dataset.setFileCount((long) remainingFiles.size()); dataset.setSizeBytes(remainingFiles.stream() .mapToLong(f -> f.getFileSize() != null ? f.getFileSize().longValue() : 0L) .sum()); @@ -310,7 +310,7 @@ public void batchDeleteFiles(String datasetId, BatchDeleteFilesRequest request) // 重新计算文件统计信息(确保数据准确) List remainingFiles = datasetFileRepository.findAllByDatasetId(datasetId); - dataset.setFileCount(remainingFiles.size()); + dataset.setFileCount((long) remainingFiles.size()); dataset.setSizeBytes(remainingFiles.stream() .mapToLong(f -> f.getFileSize() != null ? f.getFileSize().longValue() : 0L) .sum()); @@ -731,7 +731,7 @@ public void deleteDirectory(String datasetId, String prefix) { // 重新计算文件统计信息(确保数据准确) List remainingFiles = datasetFileRepository.findAllByDatasetId(datasetId); - dataset.setFileCount(remainingFiles.size()); + dataset.setFileCount((long) remainingFiles.size()); dataset.setSizeBytes(remainingFiles.stream() .mapToLong(f -> f.getFileSize() != null ? f.getFileSize().longValue() : 0L) .sum()); diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index 0010260cc..855869cc5 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -379,6 +379,8 @@ "deleteDatasetDesc": "Are you sure you want to delete \"{{itemName}}\"? This action cannot be undone.", "deleteFolderTitle": "Confirm delete folder?", "deleteFolderDesc": "Deleting folder \"{{name}}\" will remove all files and subfolders. This action cannot be undone.", + "batchDeleteTitle": "Confirm batch delete files?", + "batchDeleteDesc": "Are you sure you want to delete the selected {{count}} file(s)/folder(s)? This action cannot be undone.", "deleteConfirm": "Delete", "deleteCancel": "Cancel" }, diff --git a/frontend/src/i18n/locales/zh/common.json b/frontend/src/i18n/locales/zh/common.json index b9826e634..2d4d03138 100644 --- a/frontend/src/i18n/locales/zh/common.json +++ b/frontend/src/i18n/locales/zh/common.json @@ -379,6 +379,8 @@ "deleteDatasetDesc": "确定要删除「{{itemName}}」吗?删除后将无法恢复。", "deleteFolderTitle": "确认删除文件夹?", "deleteFolderDesc": "删除文件夹 \"{{name}}\" 将同时删除其中的所有文件和子文件夹,此操作不可恢复。", + "batchDeleteTitle": "确认批量删除文件?", + "batchDeleteDesc": "确定要删除选中的 {{count}} 个文件/文件夹吗?删除后将无法恢复。", "deleteConfirm": "删除", "deleteCancel": "取消" }, diff --git a/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx b/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx index ef0829fb1..a10f589a2 100644 --- a/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx +++ b/frontend/src/pages/DataManagement/Detail/DatasetDetail.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from "react"; -import { Breadcrumb, App, Tabs, Drawer, Descriptions } from "antd"; +import { Breadcrumb, App, Tabs, Drawer, Descriptions, Modal } from "antd"; import { Info, Edit, @@ -38,6 +38,7 @@ export default function DatasetDetail() { const datasetTypeMap = getDatasetTypeMap(t); const [showEditDialog, setShowEditDialog] = useState(false); const [showDetailDrawer, setShowDetailDrawer] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); const [dataset, setDataset] = useState({} as Dataset); @@ -179,15 +180,8 @@ export default function DatasetDetail() { key: "delete", label: t("dataManagement.actions.delete"), danger: true, - confirm: { - title: t("dataManagement.confirm.deleteDatasetTitle"), - description: t("dataManagement.confirm.deleteDatasetDesc"), - okText: t("dataManagement.confirm.deleteConfirm"), - cancelText: t("dataManagement.confirm.deleteCancel"), - okType: "danger", - }, icon: , - onClick: handleDeleteDataset, + onClick: () => setShowDeleteModal(true), }, ]; @@ -336,6 +330,22 @@ export default function DatasetDetail() { ]} /> + + {/* 删除数据集确认弹窗 */} + { + setShowDeleteModal(false); + await handleDeleteDataset(); + }} + onCancel={() => setShowDeleteModal(false)} + okText={t("dataManagement.confirm.deleteConfirm")} + cancelText={t("dataManagement.confirm.deleteCancel")} + okButtonProps={{ danger: true }} + > +

{t("dataManagement.confirm.deleteDatasetDesc", { itemName: dataset.name || t("dataManagement.detail.title") })}

+
); } diff --git a/frontend/src/pages/DataManagement/Detail/components/Overview.tsx b/frontend/src/pages/DataManagement/Detail/components/Overview.tsx index f4a525336..330d1a923 100644 --- a/frontend/src/pages/DataManagement/Detail/components/Overview.tsx +++ b/frontend/src/pages/DataManagement/Detail/components/Overview.tsx @@ -15,8 +15,9 @@ export default function Overview({ dataset, filesOperation, fetchDataset }) { // 删除确认弹窗状态 const [deleteModal, setDeleteModal] = useState<{ visible: boolean; - type: "file" | "directory"; + type: "file" | "directory" | "batch"; fileName: string; + count?: number; handler: () => void | Promise; }>({ visible: false, @@ -25,6 +26,15 @@ export default function Overview({ dataset, filesOperation, fetchDataset }) { handler: () => {}, }); + // 批量删除确认弹窗状态 + const [batchDeleteModal, setBatchDeleteModal] = useState<{ + visible: boolean; + count: number; + }>({ + visible: false, + count: 0, + }); + const { fileList, pagination, @@ -87,6 +97,24 @@ export default function Overview({ dataset, filesOperation, fetchDataset }) { setDeleteModal({ visible: false, type: "file", fileName: "", handler: () => {} }); }; + // 显示批量删除确认弹窗 + const showBatchDeleteConfirm = () => { + if (selectedFiles.length === 0) { + message.warning({ content: "请先选择要删除的文件" }); + return; + } + setBatchDeleteModal({ + visible: true, + count: selectedFiles.length, + }); + }; + + // 执行批量删除 + const handleConfirmBatchDelete = async () => { + setBatchDeleteModal({ visible: false, count: 0 }); + await handleBatchDeleteFiles(); + }; + // 文件列表列定义 const columns = [ { @@ -472,7 +500,7 @@ export default function Overview({ dataset, filesOperation, fetchDataset }) { {t("dataManagement.actions.batchExport")}