Skip to content

Commit 4b7d648

Browse files
authored
fix: data annotation task creation bug (#427)
🐛 data annotation task creation bug
1 parent 3557c0c commit 4b7d648

File tree

4 files changed

+181
-74
lines changed

4 files changed

+181
-74
lines changed

frontend/src/components/business/DatasetFileTransfer.tsx

Lines changed: 133 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -183,53 +183,122 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
183183
options?: Partial<{ page: number; pageSize: number; keyword: string }>
184184
) => {
185185
if (!selectedDataset) return;
186-
const page = options?.page ?? filesPagination.current;
187-
const pageSize = options?.pageSize ?? filesPagination.pageSize;
186+
const page = options?.page ?? 1;
187+
const pageSize = 10;
188188
const keyword = options?.keyword ?? filesSearch;
189+
const hasExtensionFilter = allowedFileExtensions && allowedFileExtensions.length > 0;
189190

190-
const { data } = await queryDatasetFilesUsingGet(selectedDataset.id, {
191-
page,
192-
size: pageSize,
193-
keyword,
194-
});
195-
const mapped = (data.content || []).map((item: DatasetFile) => ({
196-
...item,
197-
id: item.id,
198-
key: String(item.id), // rowKey 使用字符串,确保与 selectedRowKeys 类型一致
199-
// 记录所属数据集,方便后续在“全不选”时只清空当前数据集的选择
200-
// DatasetFile 接口是后端模型,这里在前端扩展 datasetId 字段
201-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
202-
// @ts-ignore
203-
datasetId: selectedDataset.id,
204-
datasetName: selectedDataset.name,
205-
}));
206-
207-
const filtered =
208-
allowedFileExtensions && allowedFileExtensions.length > 0
209-
? mapped.filter((file) => {
210-
const ext =
211-
file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
212-
return allowedFileExtensions.includes(ext);
213-
})
214-
: mapped;
215-
216-
setFiles(filtered);
217-
setFilesPagination((prev) => ({
218-
...prev,
219-
current: page,
220-
pageSize,
221-
total: data.totalElements,
222-
}));
191+
console.log('[fetchFiles] 调用:', { datasetId: selectedDataset.id, page, pageSize, keyword, hasExtensionFilter });
192+
193+
if (hasExtensionFilter) {
194+
// 有扩展名过滤:获取所有文件并在前端分页
195+
const fetchPageSize = 100;
196+
let allFiles: DatasetFile[] = [];
197+
let currentPage = 1;
198+
let hasMore = true;
199+
200+
console.log('[fetchFiles] 开始获取所有文件...');
201+
202+
while (hasMore) {
203+
const { data } = await queryDatasetFilesUsingGet(selectedDataset.id, {
204+
page: currentPage,
205+
size: fetchPageSize,
206+
keyword,
207+
});
208+
209+
const mapped = (data.content || []).map((item: DatasetFile) => ({
210+
...item,
211+
id: item.id,
212+
key: String(item.id),
213+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
214+
// @ts-ignore
215+
datasetId: selectedDataset.id,
216+
datasetName: selectedDataset.name,
217+
}));
218+
219+
allFiles = [...allFiles, ...mapped];
220+
console.log(`[fetchFiles] 第 ${currentPage} 页: 获取 ${mapped.length} 个,累计 ${allFiles.length} 个`);
221+
222+
// 如果返回的数据少于 pageSize,说明最后一页
223+
if (mapped.length < fetchPageSize) {
224+
hasMore = false;
225+
} else {
226+
currentPage++;
227+
}
228+
}
229+
230+
console.log('[fetchFiles] 共获取:', allFiles.length, '个文件');
231+
232+
// 在前端过滤
233+
const filtered = allFiles.filter((file) => {
234+
const ext = file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
235+
return allowedFileExtensions.includes(ext);
236+
});
237+
238+
console.log('[fetchFiles] 前端过滤:', {
239+
totalFiles: allFiles.length,
240+
filtered: filtered.length,
241+
extensions: allowedFileExtensions
242+
});
243+
244+
// 前端分页
245+
const startIndex = (page - 1) * pageSize;
246+
const endIndex = startIndex + pageSize;
247+
const paginatedFiles = filtered.slice(startIndex, endIndex);
248+
249+
console.log('[fetchFiles] 前端分页:', {
250+
page,
251+
startIndex,
252+
endIndex,
253+
display: paginatedFiles.length,
254+
total: filtered.length
255+
});
256+
257+
setFiles(paginatedFiles);
258+
setFilesPagination({
259+
current: page,
260+
pageSize: pageSize,
261+
total: filtered.length,
262+
});
263+
} else {
264+
// 无扩展名过滤:使用后端分页
265+
const { data } = await queryDatasetFilesUsingGet(selectedDataset.id, {
266+
page,
267+
size: pageSize,
268+
keyword,
269+
});
270+
271+
const mapped = (data.content || []).map((item: DatasetFile) => ({
272+
...item,
273+
id: item.id,
274+
key: String(item.id),
275+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
276+
// @ts-ignore
277+
datasetId: selectedDataset.id,
278+
datasetName: selectedDataset.name,
279+
}));
280+
281+
console.log('[fetchFiles] 后端分页:', {
282+
totalElements: data.totalElements,
283+
contentLength: mapped.length
284+
});
285+
286+
setFiles(mapped);
287+
setFilesPagination({
288+
current: page,
289+
pageSize: pageSize,
290+
total: data.totalElements,
291+
});
292+
}
223293
},
224-
[selectedDataset, filesPagination.current, filesPagination.pageSize, filesSearch, allowedFileExtensions]
294+
[selectedDataset, filesSearch, allowedFileExtensions]
225295
);
226296

227297
useEffect(() => {
228-
// 当数据集变化时,重置文件分页并拉取第一页文件,避免额外的循环请求
298+
// 当数据集变化时,重置文件分页并拉取第一页文件
229299
if (selectedDataset) {
230300
setFilesPagination({ current: 1, pageSize: 10, total: 0 });
231-
// 与其它页面保持一致,后端使用 1-based page 参数,这里传 1 获取第一页
232-
fetchFiles({ page: 1, pageSize: 10 }).catch(() => {});
301+
fetchFiles({ page: 1 }).catch(() => {});
233302
} else {
234303
setFiles([]);
235304
setFilesPagination({ current: 1, pageSize: 10, total: 0 });
@@ -242,6 +311,24 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
242311
onDatasetSelect?.(selectedDataset);
243312
}, [selectedDataset, onDatasetSelect]);
244313

314+
// 当搜索关键词变化时,重新拉取
315+
useEffect(() => {
316+
if (selectedDataset) {
317+
setFilesPagination({ current: 1, pageSize: 10, total: 0 });
318+
fetchFiles({ page: 1 }).catch(() => {});
319+
}
320+
// eslint-disable-next-line react-hooks/exhaustive-deps
321+
}, [filesSearch]);
322+
323+
// 当扩展名过滤变化时,重新拉取
324+
useEffect(() => {
325+
if (selectedDataset) {
326+
setFilesPagination({ current: 1, pageSize: 10, total: 0 });
327+
fetchFiles({ page: 1 }).catch(() => {});
328+
}
329+
// eslint-disable-next-line react-hooks/exhaustive-deps
330+
}, [allowedFileExtensions]);
331+
245332
// 在 fixedDatasetId 场景下,数据集列表加载完成后自动选中该数据集
246333
useEffect(() => {
247334
if (!open) return;
@@ -537,17 +624,17 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
537624
dataSource={files}
538625
columns={fileCols.slice(1, fileCols.length)}
539626
pagination={{
540-
...filesPagination,
541-
onChange: (page, pageSize) => {
542-
if (disabled) return;
543-
const nextPageSize = pageSize || filesPagination.pageSize;
627+
current: filesPagination.current,
628+
pageSize: 10,
629+
total: filesPagination.total,
630+
showSizeChanger: false,
631+
onChange: (page) => {
632+
if (disabled) return;
544633
setFilesPagination((prev) => ({
545634
...prev,
546635
current: page,
547-
pageSize: nextPageSize,
548636
}));
549-
// 前端分页与后端统一使用 1-based page 参数
550-
fetchFiles({ page, pageSize: nextPageSize }).catch(() => {});
637+
fetchFiles({ page }).catch(() => {});
551638
},
552639
}}
553640
onRow={(record: DatasetFile) => ({

frontend/src/i18n/locales/en/common.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1157,7 +1157,8 @@
11571157
"createFailed": "Failed to create annotation task, please try again",
11581158
"autoCreateSuccess": "Auto annotation task created successfully",
11591159
"autoCreateFailed": "Failed to create auto annotation task",
1160-
"datasetTypeFiltered": "Datasets have been filtered by template type, please re-select dataset and files"
1160+
"datasetTypeFiltered": "Datasets have been filtered by template type, please re-select dataset and files",
1161+
"datasetTypeMismatch": "Template type does not match dataset type, please select a matching template or dataset"
11611162
}
11621163
},
11631164
"template": {

frontend/src/i18n/locales/zh/common.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1157,7 +1157,8 @@
11571157
"createFailed": "创建失败,请稍后重试",
11581158
"autoCreateSuccess": "自动标注任务创建成功",
11591159
"autoCreateFailed": "创建自动标注任务失败",
1160-
"datasetTypeFiltered": "已根据模板类型筛选数据集,请重新选择数据集和文件"
1160+
"datasetTypeFiltered": "已根据模板类型筛选数据集,请重新选择数据集和文件",
1161+
"datasetTypeMismatch": "模板类型与数据集类型不匹配,请选择匹配的模板或数据集"
11611162
}
11621163
},
11631164
"template": {

frontend/src/pages/DataAnnotation/Create/components/CreateAnnotationTaskDialog.tsx

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
22
import { mapDataset } from "@/pages/DataManagement/dataset.const";
33
import { Button, Form, Input, Modal, Select, message, Tabs, Slider, Checkbox } from "antd";
44
import TextArea from "antd/es/input/TextArea";
5-
import { useEffect, useState } from "react";
5+
import { useEffect, useState, useRef } from "react";
66
import {
77
createAnnotationTaskUsingPost,
88
queryAnnotationTemplatesUsingGet,
@@ -123,6 +123,22 @@ export default function CreateAnnotationTask({
123123
const [imageFileCount, setImageFileCount] = useState(0);
124124
const [manualDatasetTypeFilter, setManualDatasetTypeFilter] = useState<DatasetType | undefined>(undefined);
125125
const [manualAllowedExtensions, setManualAllowedExtensions] = useState<string[] | undefined>(undefined);
126+
const [shouldResetOnOpen, setShouldResetOnOpen] = useState(false); // 创建成功后标记需要重置
127+
128+
// 防止重复提示相同的警告
129+
const lastWarningRef = useRef<{ type: string; timestamp: number } | null>(null);
130+
const showWarningOnce = (type: string, msg: string) => {
131+
const now = Date.now();
132+
const lastWarning = lastWarningRef.current;
133+
134+
// 如果3秒内提示过相同的警告,就不再提示
135+
if (lastWarning && lastWarning.type === type && now - lastWarning.timestamp < 3000) {
136+
return;
137+
}
138+
139+
lastWarningRef.current = { type, timestamp: now };
140+
message.warning(msg);
141+
};
126142

127143
useEffect(() => {
128144
if (!open) return;
@@ -158,19 +174,26 @@ export default function CreateAnnotationTask({
158174
fetchData();
159175
}, [open]);
160176

161-
// Reset form and manual-edit flag when modal opens
177+
// Reset form when modal opens after successful creation
162178
useEffect(() => {
163179
if (open) {
164-
manualForm.resetFields();
165-
autoForm.resetFields();
166-
setNameManuallyEdited(false);
167-
setActiveMode("manual");
168-
setSelectAllClasses(true);
169-
setSelectedFilesMap({});
170-
setSelectedDataset(null);
171-
setImageFileCount(0);
180+
if (shouldResetOnOpen) {
181+
// 创建成功后重新打开,清空所有状态
182+
manualForm.resetFields();
183+
autoForm.resetFields();
184+
setNameManuallyEdited(false);
185+
setActiveMode("manual");
186+
setSelectAllClasses(true);
187+
setSelectedFilesMap({});
188+
setSelectedDataset(null);
189+
setImageFileCount(0);
190+
setManualDatasetTypeFilter(undefined);
191+
setManualAllowedExtensions(undefined);
192+
setShouldResetOnOpen(false);
193+
}
194+
// 取消后重新打开,保留之前的状态,不做任何操作
172195
}
173-
}, [open, manualForm, autoForm]);
196+
}, [open, shouldResetOnOpen]);
174197

175198
useEffect(() => {
176199
const imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"];
@@ -294,6 +317,7 @@ export default function CreateAnnotationTask({
294317

295318
await createAnnotationTaskUsingPost(requestData);
296319
message?.success?.(t('dataAnnotation.create.messages.createSuccess'));
320+
setShouldResetOnOpen(true); // 标记下次打开时需要重置
297321
onClose();
298322
onRefresh();
299323
} catch (err: any) {
@@ -352,6 +376,7 @@ export default function CreateAnnotationTask({
352376

353377
await createAutoAnnotationTaskUsingPost(payload);
354378
message.success(t('dataAnnotation.create.messages.autoCreateSuccess'));
379+
setShouldResetOnOpen(true); // 标记下次打开时需要重置
355380
// 触发上层刷新自动标注任务列表
356381
(onRefresh as any)?.("auto");
357382
onClose();
@@ -440,18 +465,11 @@ export default function CreateAnnotationTask({
440465
showSearch
441466
optionFilterProp="label"
442467
notFoundContent={templates.length === 0 ? t('dataAnnotation.create.form.noTemplatesFound') : t('dataAnnotation.create.form.noTemplatesAvailable')}
443-
options={templates
444-
.filter((template) => {
445-
const tplType = mapTemplateDataTypeToDatasetType(template.dataType);
446-
if (!selectedDataset || !selectedDataset.datasetType) return true;
447-
if (!tplType) return true;
448-
return tplType === selectedDataset.datasetType;
449-
})
450-
.map((template) => ({
451-
label: template.name,
452-
value: template.id,
453-
title: template.description,
454-
}))}
468+
options={templates.map((template) => ({
469+
label: template.name,
470+
value: template.id,
471+
title: template.description,
472+
}))}
455473
onChange={(value) => {
456474
manualForm.setFieldsValue({ templateId: value });
457475

@@ -467,8 +485,9 @@ export default function CreateAnnotationTask({
467485
setSelectedDataset(null);
468486
setSelectedFilesMap({});
469487
manualForm.setFieldsValue({ datasetId: "" });
470-
message.warning(t('dataAnnotation.create.messages.datasetTypeFiltered'));
488+
showWarningOnce('datasetTypeFiltered', t('dataAnnotation.create.messages.datasetTypeFiltered'));
471489
}
490+
// 注意:不要清空 selectedFilesMap,因为文件扩展名过滤变化后,之前选择的文件可能仍然有效
472491
}}
473492
optionRender={(option) => (
474493
<div>
@@ -483,7 +502,7 @@ export default function CreateAnnotationTask({
483502
/>
484503
</Form.Item>
485504

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

0 commit comments

Comments
 (0)