diff --git a/frontend/src/components/business/DatasetFileTransfer.tsx b/frontend/src/components/business/DatasetFileTransfer.tsx index 8625285a..dde21269 100644 --- a/frontend/src/components/business/DatasetFileTransfer.tsx +++ b/frontend/src/components/business/DatasetFileTransfer.tsx @@ -183,53 +183,122 @@ const DatasetFileTransfer: React.FC = ({ options?: Partial<{ page: number; pageSize: number; keyword: string }> ) => { if (!selectedDataset) return; - const page = options?.page ?? filesPagination.current; - const pageSize = options?.pageSize ?? filesPagination.pageSize; + const page = options?.page ?? 1; + const pageSize = 10; const keyword = options?.keyword ?? filesSearch; + const hasExtensionFilter = allowedFileExtensions && allowedFileExtensions.length > 0; - const { data } = await queryDatasetFilesUsingGet(selectedDataset.id, { - page, - size: pageSize, - keyword, - }); - const mapped = (data.content || []).map((item: DatasetFile) => ({ - ...item, - id: item.id, - key: String(item.id), // rowKey 使用字符串,确保与 selectedRowKeys 类型一致 - // 记录所属数据集,方便后续在“全不选”时只清空当前数据集的选择 - // DatasetFile 接口是后端模型,这里在前端扩展 datasetId 字段 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - datasetId: selectedDataset.id, - datasetName: selectedDataset.name, - })); - - const filtered = - allowedFileExtensions && allowedFileExtensions.length > 0 - ? mapped.filter((file) => { - const ext = - file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || ""; - return allowedFileExtensions.includes(ext); - }) - : mapped; - - setFiles(filtered); - setFilesPagination((prev) => ({ - ...prev, - current: page, - pageSize, - total: data.totalElements, - })); + console.log('[fetchFiles] 调用:', { datasetId: selectedDataset.id, page, pageSize, keyword, hasExtensionFilter }); + + if (hasExtensionFilter) { + // 有扩展名过滤:获取所有文件并在前端分页 + const fetchPageSize = 100; + let allFiles: DatasetFile[] = []; + let currentPage = 1; + let hasMore = true; + + console.log('[fetchFiles] 开始获取所有文件...'); + + while (hasMore) { + const { data } = await queryDatasetFilesUsingGet(selectedDataset.id, { + page: currentPage, + size: fetchPageSize, + keyword, + }); + + const mapped = (data.content || []).map((item: DatasetFile) => ({ + ...item, + id: item.id, + key: String(item.id), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + datasetId: selectedDataset.id, + datasetName: selectedDataset.name, + })); + + allFiles = [...allFiles, ...mapped]; + console.log(`[fetchFiles] 第 ${currentPage} 页: 获取 ${mapped.length} 个,累计 ${allFiles.length} 个`); + + // 如果返回的数据少于 pageSize,说明最后一页 + if (mapped.length < fetchPageSize) { + hasMore = false; + } else { + currentPage++; + } + } + + console.log('[fetchFiles] 共获取:', allFiles.length, '个文件'); + + // 在前端过滤 + const filtered = allFiles.filter((file) => { + const ext = file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || ""; + return allowedFileExtensions.includes(ext); + }); + + console.log('[fetchFiles] 前端过滤:', { + totalFiles: allFiles.length, + filtered: filtered.length, + extensions: allowedFileExtensions + }); + + // 前端分页 + const startIndex = (page - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedFiles = filtered.slice(startIndex, endIndex); + + console.log('[fetchFiles] 前端分页:', { + page, + startIndex, + endIndex, + display: paginatedFiles.length, + total: filtered.length + }); + + setFiles(paginatedFiles); + setFilesPagination({ + current: page, + pageSize: pageSize, + total: filtered.length, + }); + } else { + // 无扩展名过滤:使用后端分页 + const { data } = await queryDatasetFilesUsingGet(selectedDataset.id, { + page, + size: pageSize, + keyword, + }); + + const mapped = (data.content || []).map((item: DatasetFile) => ({ + ...item, + id: item.id, + key: String(item.id), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + datasetId: selectedDataset.id, + datasetName: selectedDataset.name, + })); + + console.log('[fetchFiles] 后端分页:', { + totalElements: data.totalElements, + contentLength: mapped.length + }); + + setFiles(mapped); + setFilesPagination({ + current: page, + pageSize: pageSize, + total: data.totalElements, + }); + } }, - [selectedDataset, filesPagination.current, filesPagination.pageSize, filesSearch, allowedFileExtensions] + [selectedDataset, filesSearch, allowedFileExtensions] ); useEffect(() => { - // 当数据集变化时,重置文件分页并拉取第一页文件,避免额外的循环请求 + // 当数据集变化时,重置文件分页并拉取第一页文件 if (selectedDataset) { setFilesPagination({ current: 1, pageSize: 10, total: 0 }); - // 与其它页面保持一致,后端使用 1-based page 参数,这里传 1 获取第一页 - fetchFiles({ page: 1, pageSize: 10 }).catch(() => {}); + fetchFiles({ page: 1 }).catch(() => {}); } else { setFiles([]); setFilesPagination({ current: 1, pageSize: 10, total: 0 }); @@ -242,6 +311,24 @@ const DatasetFileTransfer: React.FC = ({ onDatasetSelect?.(selectedDataset); }, [selectedDataset, onDatasetSelect]); + // 当搜索关键词变化时,重新拉取 + useEffect(() => { + if (selectedDataset) { + setFilesPagination({ current: 1, pageSize: 10, total: 0 }); + fetchFiles({ page: 1 }).catch(() => {}); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filesSearch]); + + // 当扩展名过滤变化时,重新拉取 + useEffect(() => { + if (selectedDataset) { + setFilesPagination({ current: 1, pageSize: 10, total: 0 }); + fetchFiles({ page: 1 }).catch(() => {}); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allowedFileExtensions]); + // 在 fixedDatasetId 场景下,数据集列表加载完成后自动选中该数据集 useEffect(() => { if (!open) return; @@ -537,17 +624,17 @@ const DatasetFileTransfer: React.FC = ({ dataSource={files} columns={fileCols.slice(1, fileCols.length)} pagination={{ - ...filesPagination, - onChange: (page, pageSize) => { - if (disabled) return; - const nextPageSize = pageSize || filesPagination.pageSize; + current: filesPagination.current, + pageSize: 10, + total: filesPagination.total, + showSizeChanger: false, + onChange: (page) => { + if (disabled) return; setFilesPagination((prev) => ({ ...prev, current: page, - pageSize: nextPageSize, })); - // 前端分页与后端统一使用 1-based page 参数 - fetchFiles({ page, pageSize: nextPageSize }).catch(() => {}); + fetchFiles({ page }).catch(() => {}); }, }} onRow={(record: DatasetFile) => ({ diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index 0450a337..d92a2c30 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -1145,7 +1145,8 @@ "createFailed": "Failed to create annotation task, please try again", "autoCreateSuccess": "Auto annotation task created successfully", "autoCreateFailed": "Failed to create auto annotation task", - "datasetTypeFiltered": "Datasets have been filtered by template type, please re-select dataset and files" + "datasetTypeFiltered": "Datasets have been filtered by template type, please re-select dataset and files", + "datasetTypeMismatch": "Template type does not match dataset type, please select a matching template or dataset" } }, "template": { diff --git a/frontend/src/i18n/locales/zh/common.json b/frontend/src/i18n/locales/zh/common.json index 575abac3..845046cc 100644 --- a/frontend/src/i18n/locales/zh/common.json +++ b/frontend/src/i18n/locales/zh/common.json @@ -1145,7 +1145,8 @@ "createFailed": "创建失败,请稍后重试", "autoCreateSuccess": "自动标注任务创建成功", "autoCreateFailed": "创建自动标注任务失败", - "datasetTypeFiltered": "已根据模板类型筛选数据集,请重新选择数据集和文件" + "datasetTypeFiltered": "已根据模板类型筛选数据集,请重新选择数据集和文件", + "datasetTypeMismatch": "模板类型与数据集类型不匹配,请选择匹配的模板或数据集" } }, "template": { diff --git a/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx b/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx index ee86bcfa..abb265a2 100644 --- a/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx +++ b/frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx @@ -2,7 +2,7 @@ import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api"; import { mapDataset } from "@/pages/DataManagement/dataset.const"; import { Button, Form, Input, Modal, Select, message, Tabs, Slider, Checkbox } from "antd"; import TextArea from "antd/es/input/TextArea"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { createAnnotationTaskUsingPost, queryAnnotationTemplatesUsingGet, @@ -123,6 +123,22 @@ export default function CreateAnnotationTask({ const [imageFileCount, setImageFileCount] = useState(0); const [manualDatasetTypeFilter, setManualDatasetTypeFilter] = useState(undefined); const [manualAllowedExtensions, setManualAllowedExtensions] = useState(undefined); + const [shouldResetOnOpen, setShouldResetOnOpen] = useState(false); // 创建成功后标记需要重置 + + // 防止重复提示相同的警告 + const lastWarningRef = useRef<{ type: string; timestamp: number } | null>(null); + const showWarningOnce = (type: string, msg: string) => { + const now = Date.now(); + const lastWarning = lastWarningRef.current; + + // 如果3秒内提示过相同的警告,就不再提示 + if (lastWarning && lastWarning.type === type && now - lastWarning.timestamp < 3000) { + return; + } + + lastWarningRef.current = { type, timestamp: now }; + message.warning(msg); + }; useEffect(() => { if (!open) return; @@ -158,19 +174,26 @@ export default function CreateAnnotationTask({ fetchData(); }, [open]); - // Reset form and manual-edit flag when modal opens + // Reset form when modal opens after successful creation useEffect(() => { if (open) { - manualForm.resetFields(); - autoForm.resetFields(); - setNameManuallyEdited(false); - setActiveMode("manual"); - setSelectAllClasses(true); - setSelectedFilesMap({}); - setSelectedDataset(null); - setImageFileCount(0); + if (shouldResetOnOpen) { + // 创建成功后重新打开,清空所有状态 + manualForm.resetFields(); + autoForm.resetFields(); + setNameManuallyEdited(false); + setActiveMode("manual"); + setSelectAllClasses(true); + setSelectedFilesMap({}); + setSelectedDataset(null); + setImageFileCount(0); + setManualDatasetTypeFilter(undefined); + setManualAllowedExtensions(undefined); + setShouldResetOnOpen(false); + } + // 取消后重新打开,保留之前的状态,不做任何操作 } - }, [open, manualForm, autoForm]); + }, [open, shouldResetOnOpen]); useEffect(() => { const imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"]; @@ -294,6 +317,7 @@ export default function CreateAnnotationTask({ await createAnnotationTaskUsingPost(requestData); message?.success?.(t('dataAnnotation.create.messages.createSuccess')); + setShouldResetOnOpen(true); // 标记下次打开时需要重置 onClose(); onRefresh(); } catch (err: any) { @@ -352,6 +376,7 @@ export default function CreateAnnotationTask({ await createAutoAnnotationTaskUsingPost(payload); message.success(t('dataAnnotation.create.messages.autoCreateSuccess')); + setShouldResetOnOpen(true); // 标记下次打开时需要重置 // 触发上层刷新自动标注任务列表 (onRefresh as any)?.("auto"); onClose(); @@ -440,18 +465,11 @@ export default function CreateAnnotationTask({ showSearch optionFilterProp="label" notFoundContent={templates.length === 0 ? t('dataAnnotation.create.form.noTemplatesFound') : t('dataAnnotation.create.form.noTemplatesAvailable')} - options={templates - .filter((template) => { - const tplType = mapTemplateDataTypeToDatasetType(template.dataType); - if (!selectedDataset || !selectedDataset.datasetType) return true; - if (!tplType) return true; - return tplType === selectedDataset.datasetType; - }) - .map((template) => ({ - label: template.name, - value: template.id, - title: template.description, - }))} + options={templates.map((template) => ({ + label: template.name, + value: template.id, + title: template.description, + }))} onChange={(value) => { manualForm.setFieldsValue({ templateId: value }); @@ -467,8 +485,9 @@ export default function CreateAnnotationTask({ setSelectedDataset(null); setSelectedFilesMap({}); manualForm.setFieldsValue({ datasetId: "" }); - message.warning(t('dataAnnotation.create.messages.datasetTypeFiltered')); + showWarningOnce('datasetTypeFiltered', t('dataAnnotation.create.messages.datasetTypeFiltered')); } + // 注意:不要清空 selectedFilesMap,因为文件扩展名过滤变化后,之前选择的文件可能仍然有效 }} optionRender={(option) => (
@@ -483,7 +502,7 @@ export default function CreateAnnotationTask({ /> - {/* 选择数据集和文件(仅允许单一数据集,多文件),需先选模板再操作 */} + {/* 选择数据集和文件(仅允许单一数据集,多文件),先选数据集,再选模板 */} {selectedDataset && (