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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class DatasetFileResponse {
private String tags;
/** 标签更新时间 */
private LocalDateTime tagsUpdatedAt;
/** 文件元数据(包含标注信息等,JSON 字符串) */
private String metadata;
/** 上传时间 */
private LocalDateTime uploadTime;
/** 最后更新时间 */
Expand Down
57 changes: 41 additions & 16 deletions frontend/src/components/business/DatasetFileTransfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ interface DatasetFileTransferProps
onSelectedFilesChange: (filesMap: { [key: string]: DatasetFile }) => void;
onDatasetSelect?: (dataset: Dataset | null) => void;
datasetTypeFilter?: DatasetType;
/**
* 允许选择的文件扩展名白名单(小写,包含点号,例如 ".jpg")。
* - 若不设置,则不过滤扩展名;
* - 若设置,则仅展示和选择这些扩展名的文件(包括“全选当前数据集”)。
*/
allowedFileExtensions?: string[];
/**
* 是否强制“单数据集模式”:
* - 为 true 时,仅允许从同一个数据集选择文件;
Expand Down Expand Up @@ -77,6 +83,7 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
onSelectedFilesChange,
onDatasetSelect,
datasetTypeFilter,
allowedFileExtensions,
singleDatasetOnly,
fixedDatasetId,
lockedFileIds,
Expand Down Expand Up @@ -180,27 +187,36 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
size: pageSize,
keyword,
});
setFiles(
(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 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,
}));
},
[selectedDataset, filesPagination.current, filesPagination.pageSize, filesSearch]
[selectedDataset, filesPagination.current, filesPagination.pageSize, filesSearch, allowedFileExtensions]
);

useEffect(() => {
Expand Down Expand Up @@ -269,7 +285,7 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
size: pageSize,
});

const content: DatasetFile[] = (data.content || []).map(
const mapped: DatasetFile[] = (data.content || []).map(
(item: DatasetFile) => ({
...item,
key: item.id,
Expand All @@ -281,6 +297,15 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({
}),
);

const content: DatasetFile[] =
allowedFileExtensions && allowedFileExtensions.length > 0
? mapped.filter((file) => {
const ext =
file.fileName?.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
return allowedFileExtensions.includes(ext);
})
: mapped;

if (!content.length) {
break;
}
Expand All @@ -306,7 +331,7 @@ const DatasetFileTransfer: React.FC<DatasetFileTransferProps> = ({

onSelectedFilesChange(newMap);

const count = total || allFiles.length;
const count = allFiles.length;
if (count > 0) {
message.success(`已选中当前数据集的全部 ${count} 个文件`);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
deleteAutoAnnotationTaskByIdUsingDelete,
downloadAutoAnnotationResultUsingGet,
queryAnnotationTasksUsingGet,
syncAutoAnnotationToDatabaseUsingPost,
} from "../annotation.api";
import CreateAutoAnnotationDialog from "./components/CreateAutoAnnotationDialog";
import EditAutoAnnotationDatasetDialog from "./components/EditAutoAnnotationDatasetDialog";
Expand Down Expand Up @@ -123,6 +124,32 @@ export default function AutoAnnotation() {
setShowImportDialog(true);
};

const handleSyncToDatabase = (task: AutoAnnotationTask) => {
Modal.confirm({
title: `确认将自动标注任务「${task.name}」在 Label Studio 中的标注结果同步到数据库吗?`,
content: (
<div>
<div>此操作会根据 Label Studio 中的任务数据覆盖当前文件标签与标注信息。</div>
<div>同步完成后,可在数据管理的文件详情中查看最新标签与标注。</div>
</div>
),
okText: "同步到数据库",
cancelText: "取消",
onOk: async () => {
const hide = message.loading("正在从 Label Studio 同步标注到数据库...", 0);
try {
await syncAutoAnnotationToDatabaseUsingPost(task.id);
hide();
message.success("同步完成");
} catch (e) {
console.error(e);
hide();
message.error("同步失败,请稍后重试");
}
},
});
};

const handleDelete = (task: AutoAnnotationTask) => {
Modal.confirm({
title: `确认删除自动标注任务「${task.name}」吗?`,
Expand Down Expand Up @@ -307,14 +334,14 @@ export default function AutoAnnotation() {
编辑
</Button>
</Tooltip>
<Tooltip title="从 Label Studio 同步标注结果到数据集">
<Tooltip title="从 Label Studio 同步标注结果到数据库">
<Button
type="link"
size="small"
icon={<SyncOutlined />}
onClick={() => handleImportFromLabelStudio(record)}
onClick={() => handleSyncToDatabase(record)}
>
同步
同步到数据库
</Button>
</Tooltip>

Expand Down Expand Up @@ -344,6 +371,12 @@ export default function AutoAnnotation() {
<Dropdown
menu={{
items: [
{
key: "export-result",
label: "导出标注结果",
icon: <DownloadOutlined />,
onClick: () => handleImportFromLabelStudio(record),
},
{
key: "edit-dataset",
label: "编辑任务数据集",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export default function CreateAnnotationTask({
const [selectedDataset, setSelectedDataset] = useState<Dataset | null>(null);
const [imageFileCount, setImageFileCount] = useState(0);
const [manualDatasetTypeFilter, setManualDatasetTypeFilter] = useState<DatasetType | undefined>(undefined);
const [manualAllowedExtensions, setManualAllowedExtensions] = useState<string[] | undefined>(undefined);

useEffect(() => {
if (!open) return;
Expand Down Expand Up @@ -178,6 +179,11 @@ export default function CreateAnnotationTask({
setImageFileCount(count);
}, [selectedFilesMap]);

const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"];
const TEXT_EXTENSIONS = [".txt", ".md", ".csv", ".tsv", ".jsonl", ".log"];
const AUDIO_EXTENSIONS = [".wav", ".mp3", ".flac", ".aac", ".ogg", ".m4a", ".wma"];
const VIDEO_EXTENSIONS = [".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv", ".webm"];

const mapTemplateDataTypeToDatasetType = (raw?: string): DatasetType | undefined => {
if (!raw) return undefined;
const v = String(raw).trim().toLowerCase();
Expand Down Expand Up @@ -218,6 +224,40 @@ export default function CreateAnnotationTask({
return undefined;
};

const getAllowedExtensionsForTemplateDataType = (raw?: string): string[] | undefined => {
if (!raw) return undefined;
const v = String(raw).trim().toLowerCase();

const textTokens = new Set<string>([
"text",
DataType.TEXT.toLowerCase(),
"文本",
]);
const imageTokens = new Set<string>([
"image",
DataType.IMAGE.toLowerCase(),
"图像",
"图片",
]);
const audioTokens = new Set<string>([
"audio",
DataType.AUDIO.toLowerCase(),
"音频",
]);
const videoTokens = new Set<string>([
"video",
DataType.VIDEO.toLowerCase(),
"视频",
]);

if (textTokens.has(v)) return TEXT_EXTENSIONS;
if (imageTokens.has(v)) return IMAGE_EXTENSIONS;
if (audioTokens.has(v)) return AUDIO_EXTENSIONS;
if (videoTokens.has(v)) return VIDEO_EXTENSIONS;

return undefined;
};

const handleManualSubmit = async () => {
try {
const values = await manualForm.validateFields();
Expand Down Expand Up @@ -417,6 +457,9 @@ export default function CreateAnnotationTask({
const nextType = mapTemplateDataTypeToDatasetType(tpl?.dataType);
setManualDatasetTypeFilter(nextType);

const nextExtensions = getAllowedExtensionsForTemplateDataType(tpl?.dataType);
setManualAllowedExtensions(nextExtensions);

// 若当前已选数据集类型与模板不匹配,则清空当前选择
if (selectedDataset && nextType && selectedDataset.datasetType !== nextType) {
setSelectedDataset(null);
Expand Down Expand Up @@ -459,6 +502,7 @@ export default function CreateAnnotationTask({
}
}}
datasetTypeFilter={manualDatasetTypeFilter}
allowedFileExtensions={manualAllowedExtensions}
singleDatasetOnly
disabled={!manualForm.getFieldValue("templateId")}
/>
Expand Down Expand Up @@ -512,6 +556,7 @@ export default function CreateAnnotationTask({
autoForm.setFieldsValue({ datasetId: dataset?.id ?? "" });
}}
datasetTypeFilter={DatasetType.IMAGE}
allowedFileExtensions={IMAGE_EXTENSIONS}
singleDatasetOnly
/>
{selectedDataset && (
Expand Down
Loading
Loading