Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 133 additions & 46 deletions frontend/src/components/business/DatasetFileTransfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,53 +183,122 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
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 });
Expand All @@ -242,6 +311,24 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
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;
Expand Down Expand Up @@ -537,17 +624,17 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
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) => ({
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/i18n/locales/zh/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,8 @@
"createFailed": "创建失败,请稍后重试",
"autoCreateSuccess": "自动标注任务创建成功",
"autoCreateFailed": "创建自动标注任务失败",
"datasetTypeFiltered": "已根据模板类型筛选数据集,请重新选择数据集和文件"
"datasetTypeFiltered": "已根据模板类型筛选数据集,请重新选择数据集和文件",
"datasetTypeMismatch": "模板类型与数据集类型不匹配,请选择匹配的模板或数据集"
}
},
"template": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -123,6 +123,22 @@ export default function CreateAnnotationTask({
const [imageFileCount, setImageFileCount] = useState(0);
const [manualDatasetTypeFilter, setManualDatasetTypeFilter] = useState<DatasetType | undefined>(undefined);
const [manualAllowedExtensions, setManualAllowedExtensions] = useState<string[] | undefined>(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;
Expand Down Expand Up @@ -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"];
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 });

Expand All @@ -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) => (
<div>
Expand All @@ -483,7 +502,7 @@ export default function CreateAnnotationTask({
/>
</Form.Item>

{/* 选择数据集和文件(仅允许单一数据集,多文件),需先选模板再操作 */}
{/* 选择数据集和文件(仅允许单一数据集,多文件),先选数据集,再选模板 */}
<Form.Item label={t('dataAnnotation.create.form.selectDatasetAndFiles')} required>
<DatasetFileTransfer
open
Expand All @@ -506,7 +525,6 @@ export default function CreateAnnotationTask({
datasetTypeFilter={manualDatasetTypeFilter}
allowedFileExtensions={manualAllowedExtensions}
singleDatasetOnly
disabled={!manualForm.getFieldValue("templateId")}
/>
{selectedDataset && (
<div className="mt-2 p-2 bg-blue-50 rounded border border-blue-200 text-xs">
Expand Down
Loading