Compare commits

...

2 Commits

Author SHA1 Message Date
4a3e466210 feat(annotation): 添加标注任务进行中数据显示功能
- 新增 AnnotationTaskListItem 和相关类型定义
- 在前端页面中添加标注中列显示进行中的标注数据量
- 更新数据获取逻辑以支持进行中标注数量统计
- 修改后端服务层添加 in_progress_count 字段映射
- 优化类型安全和代码结构设计
2026-01-31 17:14:23 +08:00
5d8d25ca8c fix(annotation): 解决空标注结果的状态处理问题
- 在构建标注快照时增加空标注检查,避免空对象被处理
- 修改状态判断逻辑,当标注为空且当前状态为 NO_ANNOTATION 或 NOT_APPLICABLE 时保持原状态
- 移除冗余的 hasExistingAnnotation 变量检查
- 确保空标注情况下状态流转的正确性,防止误标为已标注状态
2026-01-31 16:57:38 +08:00
5 changed files with 152 additions and 35 deletions

View File

@@ -192,6 +192,7 @@ const stableStringify = (value: unknown) => {
const buildAnnotationSnapshot = (annotation?: Record<string, unknown>) => { const buildAnnotationSnapshot = (annotation?: Record<string, unknown>) => {
if (!annotation) return ""; if (!annotation) return "";
if (isAnnotationResultEmpty(annotation)) return "";
const cleaned: Record<string, unknown> = { ...annotation }; const cleaned: Record<string, unknown> = { ...annotation };
delete cleaned.updated_at; delete cleaned.updated_at;
delete cleaned.updatedAt; delete cleaned.updatedAt;
@@ -717,11 +718,13 @@ export default function LabelStudioTextEditor() {
const annotationRecord = annotation as Record<string, unknown>; const annotationRecord = annotation as Record<string, unknown>;
const currentTask = tasks.find((item) => item.fileId === String(fileId)); const currentTask = tasks.find((item) => item.fileId === String(fileId));
const currentStatus = currentTask?.annotationStatus; const currentStatus = currentTask?.annotationStatus;
const hasExistingAnnotation = !!currentTask?.hasAnnotation;
let resolvedStatus: AnnotationResultStatus; let resolvedStatus: AnnotationResultStatus;
if (isAnnotationResultEmpty(annotationRecord)) { if (isAnnotationResultEmpty(annotationRecord)) {
if (currentStatus === AnnotationResultStatus.ANNOTATED || (hasExistingAnnotation && !currentStatus)) { if (
resolvedStatus = AnnotationResultStatus.ANNOTATED; currentStatus === AnnotationResultStatus.NO_ANNOTATION ||
currentStatus === AnnotationResultStatus.NOT_APPLICABLE
) {
resolvedStatus = currentStatus;
} else { } else {
const selectedStatus = await confirmEmptyAnnotationStatus(); const selectedStatus = await confirmEmptyAnnotationStatus();
if (!selectedStatus) return false; if (!selectedStatus) return false;
@@ -1033,6 +1036,15 @@ export default function LabelStudioTextEditor() {
[segmentTreeData] [segmentTreeData]
); );
const inProgressSegmentedCount = useMemo(() => {
if (tasks.length === 0) return 0;
return tasks.reduce((count, item) => {
const summary = resolveSegmentSummary(item);
if (!summary) return count;
return summary.done < summary.total ? count + 1 : count;
}, 0);
}, [tasks]);
const handleSegmentSelect = useCallback((keys: Array<string | number>) => { const handleSegmentSelect = useCallback((keys: Array<string | number>) => {
const [first] = keys; const [first] = keys;
if (first === undefined || first === null) return; if (first === undefined || first === null) return;
@@ -1214,8 +1226,13 @@ export default function LabelStudioTextEditor() {
className="border-r border-gray-200 bg-gray-50 flex flex-col transition-all duration-200 min-h-0" className="border-r border-gray-200 bg-gray-50 flex flex-col transition-all duration-200 min-h-0"
style={{ width: sidebarCollapsed ? 0 : 240, overflow: "hidden" }} style={{ width: sidebarCollapsed ? 0 : 240, overflow: "hidden" }}
> >
<div className="px-3 py-2 border-b border-gray-200 bg-white font-medium text-sm"> <div className="px-3 py-2 border-b border-gray-200 bg-white font-medium text-sm flex items-center justify-between gap-2">
<span></span>
{segmented && (
<Tag color="orange" style={{ margin: 0 }}>
{inProgressSegmentedCount}
</Tag>
)}
</div> </div>
<div className="flex-1 min-h-0 overflow-auto"> <div className="flex-1 min-h-0 overflow-auto">
<List <List

View File

@@ -10,27 +10,35 @@ import {
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { SearchControls } from "@/components/SearchControls"; import { SearchControls } from "@/components/SearchControls";
import CardView from "@/components/CardView"; import CardView from "@/components/CardView";
import type { AnnotationTask } from "../annotation.model";
import useFetchData from "@/hooks/useFetchData"; import useFetchData from "@/hooks/useFetchData";
import { import {
deleteAnnotationTaskByIdUsingDelete, deleteAnnotationTaskByIdUsingDelete,
queryAnnotationTasksUsingGet, queryAnnotationTasksUsingGet,
} from "../annotation.api"; } from "../annotation.api";
import { mapAnnotationTask } from "../annotation.const"; import { mapAnnotationTask, type AnnotationTaskListItem } from "../annotation.const";
import CreateAnnotationTask from "../Create/components/CreateAnnotationTaskDialog"; import CreateAnnotationTask from "../Create/components/CreateAnnotationTaskDialog";
import ExportAnnotationDialog from "./ExportAnnotationDialog"; import ExportAnnotationDialog from "./ExportAnnotationDialog";
import { ColumnType } from "antd/es/table"; import { ColumnType } from "antd/es/table";
import { TemplateList } from "../Template"; import { TemplateList } from "../Template";
// Note: DevelopmentInProgress intentionally not used here // Note: DevelopmentInProgress intentionally not used here
type AnnotationTaskRowKey = string | number;
type AnnotationTaskOperation = {
key: string;
label: string;
icon: JSX.Element;
danger?: boolean;
onClick: (task: AnnotationTaskListItem) => void;
};
export default function DataAnnotation() { export default function DataAnnotation() {
// return <DevelopmentInProgress showTime="2025.10.30" />; // return <DevelopmentInProgress showTime="2025.10.30" />;
const navigate = useNavigate(); const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("tasks"); const [activeTab, setActiveTab] = useState("tasks");
const [viewMode, setViewMode] = useState<"list" | "card">("list"); const [viewMode, setViewMode] = useState<"list" | "card">("list");
const [showCreateDialog, setShowCreateDialog] = useState(false); const [showCreateDialog, setShowCreateDialog] = useState(false);
const [exportTask, setExportTask] = useState<AnnotationTask | null>(null); const [exportTask, setExportTask] = useState<AnnotationTaskListItem | null>(null);
const [editTask, setEditTask] = useState<AnnotationTask | null>(null); const [editTask, setEditTask] = useState<AnnotationTaskListItem | null>(null);
const { const {
loading, loading,
@@ -40,13 +48,13 @@ export default function DataAnnotation() {
fetchData, fetchData,
handleFiltersChange, handleFiltersChange,
handleKeywordChange, handleKeywordChange,
} = useFetchData(queryAnnotationTasksUsingGet, mapAnnotationTask, 30000, true, [], 0); } = useFetchData<AnnotationTaskListItem>(queryAnnotationTasksUsingGet, mapAnnotationTask, 30000, true, [], 0);
const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<AnnotationTaskRowKey[]>([]);
const [selectedRows, setSelectedRows] = useState<any[]>([]); const [selectedRows, setSelectedRows] = useState<AnnotationTaskListItem[]>([]);
const handleAnnotate = (task: AnnotationTask) => { const handleAnnotate = (task: AnnotationTaskListItem) => {
const projectId = (task as any)?.id; const projectId = task.id;
if (!projectId) { if (!projectId) {
message.error("无法进入标注:缺少标注项目ID"); message.error("无法进入标注:缺少标注项目ID");
return; return;
@@ -54,15 +62,15 @@ export default function DataAnnotation() {
navigate(`/data/annotation/annotate/${projectId}`); navigate(`/data/annotation/annotate/${projectId}`);
}; };
const handleExport = (task: AnnotationTask) => { const handleExport = (task: AnnotationTaskListItem) => {
setExportTask(task); setExportTask(task);
}; };
const handleEdit = (task: AnnotationTask) => { const handleEdit = (task: AnnotationTaskListItem) => {
setEditTask(task); setEditTask(task);
}; };
const handleDelete = (task: AnnotationTask) => { const handleDelete = (task: AnnotationTaskListItem) => {
Modal.confirm({ Modal.confirm({
title: `确认删除标注任务「${task.name}」吗?`, title: `确认删除标注任务「${task.name}」吗?`,
content: "删除标注任务不会删除对应数据集,但会删除该任务的所有标注结果。", content: "删除标注任务不会删除对应数据集,但会删除该任务的所有标注结果。",
@@ -110,7 +118,7 @@ export default function DataAnnotation() {
}); });
}; };
const operations = [ const operations: AnnotationTaskOperation[] = [
{ {
key: "annotate", key: "annotate",
label: "标注", label: "标注",
@@ -142,7 +150,7 @@ export default function DataAnnotation() {
}, },
]; ];
const columns: ColumnType<any>[] = [ const columns: ColumnType<AnnotationTaskListItem>[] = [
{ {
title: "任务名称", title: "任务名称",
dataIndex: "name", dataIndex: "name",
@@ -173,7 +181,7 @@ export default function DataAnnotation() {
key: "annotatedCount", key: "annotatedCount",
width: 100, width: 100,
align: "center" as const, align: "center" as const,
render: (value: number, record: any) => { render: (value: number, record: AnnotationTaskListItem) => {
const total = record.totalCount || 0; const total = record.totalCount || 0;
const annotated = value || 0; const annotated = value || 0;
const percent = total > 0 ? Math.round((annotated / total) * 100) : 0; const percent = total > 0 ? Math.round((annotated / total) * 100) : 0;
@@ -184,6 +192,23 @@ export default function DataAnnotation() {
); );
}, },
}, },
{
title: "标注中",
dataIndex: "inProgressCount",
key: "inProgressCount",
width: 100,
align: "center" as const,
render: (value: number, record: AnnotationTaskListItem) => {
const segmentationEnabled =
record.segmentationEnabled ?? record.segmentation_enabled;
if (!segmentationEnabled) return "-";
const resolved =
Number.isFinite(value)
? value
: record.inProgressCount ?? record.in_progress_count ?? 0;
return resolved;
},
},
{ {
title: "创建时间", title: "创建时间",
dataIndex: "createdAt", dataIndex: "createdAt",
@@ -202,14 +227,14 @@ export default function DataAnnotation() {
fixed: "right" as const, fixed: "right" as const,
width: 150, width: 150,
dataIndex: "actions", dataIndex: "actions",
render: (_: any, task: any) => ( render: (_value: unknown, task: AnnotationTaskListItem) => (
<div className="flex items-center justify-center space-x-1"> <div className="flex items-center justify-center space-x-1">
{operations.map((operation) => ( {operations.map((operation) => (
<Button <Button
key={operation.key} key={operation.key}
type="text" type="text"
icon={operation.icon} icon={operation.icon}
onClick={() => (operation?.onClick as any)?.(task)} onClick={() => operation.onClick(task)}
title={operation.label} title={operation.label}
/> />
))} ))}
@@ -282,9 +307,9 @@ export default function DataAnnotation() {
pagination={pagination} pagination={pagination}
rowSelection={{ rowSelection={{
selectedRowKeys, selectedRowKeys,
onChange: (keys, rows) => { onChange: (keys: AnnotationTaskRowKey[], rows: AnnotationTaskListItem[]) => {
setSelectedRowKeys(keys as (string | number)[]); setSelectedRowKeys(keys);
setSelectedRows(rows as any[]); setSelectedRows(rows);
}, },
}} }}
scroll={{ x: "max-content", y: "calc(100vh - 24rem)" }} scroll={{ x: "max-content", y: "calc(100vh - 24rem)" }}
@@ -293,7 +318,7 @@ export default function DataAnnotation() {
) : ( ) : (
<CardView <CardView
data={tableData} data={tableData}
operations={operations as any} operations={operations}
pagination={pagination} pagination={pagination}
loading={loading} loading={loading}
/> />
@@ -327,4 +352,4 @@ export default function DataAnnotation() {
/> />
</div> </div>
); );
} }

View File

@@ -6,6 +6,64 @@ import {
CloseCircleOutlined, CloseCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
type AnnotationTaskStatistics = {
accuracy?: number | string;
averageTime?: number | string;
reviewCount?: number | string;
};
type AnnotationTaskPayload = {
id?: string;
labelingProjId?: string;
labelingProjectId?: string;
projId?: string;
labeling_project_id?: string;
name?: string;
description?: string;
datasetId?: string;
datasetName?: string;
dataset_name?: string;
totalCount?: number;
total_count?: number;
annotatedCount?: number;
annotated_count?: number;
inProgressCount?: number;
in_progress_count?: number;
segmentationEnabled?: boolean;
segmentation_enabled?: boolean;
createdAt?: string;
created_at?: string;
updatedAt?: string;
updated_at?: string;
status?: string;
statistics?: AnnotationTaskStatistics;
[key: string]: unknown;
};
export type AnnotationTaskListItem = {
id?: string;
labelingProjId?: string;
projId?: string;
name?: string;
description?: string;
datasetId?: string;
datasetName?: string;
totalCount?: number;
annotatedCount?: number;
inProgressCount?: number;
segmentationEnabled?: boolean;
createdAt?: string;
updatedAt?: string;
icon?: JSX.Element;
iconColor?: string;
status?: {
label: string;
color: string;
};
statistics?: { label: string; value: string | number }[];
[key: string]: unknown;
};
export const AnnotationTaskStatusMap = { export const AnnotationTaskStatusMap = {
[AnnotationTaskStatus.ACTIVE]: { [AnnotationTaskStatus.ACTIVE]: {
label: "活跃", label: "活跃",
@@ -27,9 +85,11 @@ export const AnnotationTaskStatusMap = {
}, },
}; };
export function mapAnnotationTask(task: any) { export function mapAnnotationTask(task: AnnotationTaskPayload): AnnotationTaskListItem {
// Normalize labeling project id from possible backend field names // Normalize labeling project id from possible backend field names
const labelingProjId = task?.labelingProjId || task?.labelingProjectId || task?.projId || task?.labeling_project_id || ""; const labelingProjId = task?.labelingProjId || task?.labelingProjectId || task?.projId || task?.labeling_project_id || "";
const segmentationEnabled = task?.segmentationEnabled ?? task?.segmentation_enabled ?? false;
const inProgressCount = task?.inProgressCount ?? task?.in_progress_count ?? 0;
const statsArray = task?.statistics const statsArray = task?.statistics
? [ ? [
@@ -45,6 +105,8 @@ export function mapAnnotationTask(task: any) {
// provide consistent field for components // provide consistent field for components
labelingProjId, labelingProjId,
projId: labelingProjId, projId: labelingProjId,
segmentationEnabled,
inProgressCount,
name: task.name, name: task.name,
description: task.description || "", description: task.description || "",
datasetName: task.datasetName || task.dataset_name || "-", datasetName: task.datasetName || task.dataset_name || "-",
@@ -478,4 +540,4 @@ export const TemplateTypeMap = {
label: "自定义", label: "自定义",
value: TemplateType.CUSTOM value: TemplateType.CUSTOM
}, },
} }

View File

@@ -61,6 +61,7 @@ class DatasetMappingResponse(BaseModel):
) )
total_count: int = Field(0, alias="totalCount", description="数据集总数据量") total_count: int = Field(0, alias="totalCount", description="数据集总数据量")
annotated_count: int = Field(0, alias="annotatedCount", description="已标注数据量") annotated_count: int = Field(0, alias="annotatedCount", description="已标注数据量")
in_progress_count: int = Field(0, alias="inProgressCount", description="分段标注中数据量")
created_at: datetime = Field(..., alias="createdAt", description="创建时间") created_at: datetime = Field(..., alias="createdAt", description="创建时间")
updated_at: Optional[datetime] = Field(None, alias="updatedAt", description="更新时间") updated_at: Optional[datetime] = Field(None, alias="updatedAt", description="更新时间")
deleted_at: Optional[datetime] = Field(None, alias="deletedAt", description="删除时间") deleted_at: Optional[datetime] = Field(None, alias="deletedAt", description="删除时间")

View File

@@ -8,6 +8,7 @@ import uuid
from app.core.logging import get_logger from app.core.logging import get_logger
from app.db.models import LabelingProject, AnnotationTemplate, AnnotationResult, LabelingProjectFile from app.db.models import LabelingProject, AnnotationTemplate, AnnotationResult, LabelingProjectFile
from app.db.models.annotation_management import ANNOTATION_STATUS_IN_PROGRESS
from app.db.models.dataset_management import Dataset, DatasetFiles from app.db.models.dataset_management import Dataset, DatasetFiles
from app.module.annotation.schema import ( from app.module.annotation.schema import (
DatasetMappingCreateRequest, DatasetMappingCreateRequest,
@@ -40,7 +41,7 @@ class DatasetMappingService:
self, self,
project_id: str, project_id: str,
dataset_id: str dataset_id: str
) -> Tuple[int, int]: ) -> Tuple[int, int, int]:
""" """
获取项目的统计数据 获取项目的统计数据
@@ -49,7 +50,7 @@ class DatasetMappingService:
dataset_id: 数据集ID dataset_id: 数据集ID
Returns: Returns:
(total_count, annotated_count) 元组 (total_count, annotated_count, in_progress_count) 元组
""" """
# 获取标注项目快照数据量(只统计快照内的文件) # 获取标注项目快照数据量(只统计快照内的文件)
total_result = await self.db.execute( total_result = await self.db.execute(
@@ -71,7 +72,16 @@ class DatasetMappingService:
) )
annotated_count = int(annotated_result.scalar() or 0) annotated_count = int(annotated_result.scalar() or 0)
return total_count, annotated_count # 获取分段标注中数据量(标注状态为 IN_PROGRESS)
in_progress_result = await self.db.execute(
select(func.count(func.distinct(AnnotationResult.file_id))).where(
AnnotationResult.project_id == project_id,
AnnotationResult.annotation_status == ANNOTATION_STATUS_IN_PROGRESS,
)
)
in_progress_count = int(in_progress_result.scalar() or 0)
return total_count, annotated_count, in_progress_count
async def _to_response_from_row( async def _to_response_from_row(
self, self,
@@ -110,7 +120,7 @@ class DatasetMappingService:
logger.debug(f"Included template details for template_id: {template_id}") logger.debug(f"Included template details for template_id: {template_id}")
# 获取统计数据 # 获取统计数据
total_count, annotated_count = await self._get_project_stats( total_count, annotated_count, in_progress_count = await self._get_project_stats(
mapping.id, mapping.dataset_id mapping.id, mapping.dataset_id
) )
@@ -127,6 +137,7 @@ class DatasetMappingService:
"segmentation_enabled": segmentation_enabled, "segmentation_enabled": segmentation_enabled,
"total_count": total_count, "total_count": total_count,
"annotated_count": annotated_count, "annotated_count": annotated_count,
"in_progress_count": in_progress_count,
"created_at": mapping.created_at, "created_at": mapping.created_at,
"updated_at": mapping.updated_at, "updated_at": mapping.updated_at,
"deleted_at": mapping.deleted_at, "deleted_at": mapping.deleted_at,
@@ -177,9 +188,9 @@ class DatasetMappingService:
logger.debug(f"Included template details for template_id: {template_id}") logger.debug(f"Included template details for template_id: {template_id}")
# 获取统计数据 # 获取统计数据
total_count, annotated_count = 0, 0 total_count, annotated_count, in_progress_count = 0, 0, 0
if dataset_id: if dataset_id:
total_count, annotated_count = await self._get_project_stats( total_count, annotated_count, in_progress_count = await self._get_project_stats(
mapping.id, dataset_id mapping.id, dataset_id
) )
@@ -197,6 +208,7 @@ class DatasetMappingService:
"segmentation_enabled": segmentation_enabled, "segmentation_enabled": segmentation_enabled,
"total_count": total_count, "total_count": total_count,
"annotated_count": annotated_count, "annotated_count": annotated_count,
"in_progress_count": in_progress_count,
"created_at": mapping.created_at, "created_at": mapping.created_at,
"updated_at": mapping.updated_at, "updated_at": mapping.updated_at,
"deleted_at": mapping.deleted_at, "deleted_at": mapping.deleted_at,