From 4a3e466210df38552ce9e7cead5fa27688f99f6f Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 31 Jan 2026 17:14:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(annotation):=20=E6=B7=BB=E5=8A=A0=E6=A0=87?= =?UTF-8?q?=E6=B3=A8=E4=BB=BB=E5=8A=A1=E8=BF=9B=E8=A1=8C=E4=B8=AD=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=98=BE=E7=A4=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AnnotationTaskListItem 和相关类型定义 - 在前端页面中添加标注中列显示进行中的标注数据量 - 更新数据获取逻辑以支持进行中标注数量统计 - 修改后端服务层添加 in_progress_count 字段映射 - 优化类型安全和代码结构设计 --- .../DataAnnotation/Home/DataAnnotation.tsx | 69 +++++++++++++------ .../pages/DataAnnotation/annotation.const.tsx | 66 +++++++++++++++++- .../app/module/annotation/schema/mapping.py | 1 + .../app/module/annotation/service/mapping.py | 24 +++++-- 4 files changed, 130 insertions(+), 30 deletions(-) diff --git a/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx b/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx index a82e888..3cbdc45 100644 --- a/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx +++ b/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx @@ -10,27 +10,35 @@ import { import { useNavigate } from "react-router"; import { SearchControls } from "@/components/SearchControls"; import CardView from "@/components/CardView"; -import type { AnnotationTask } from "../annotation.model"; import useFetchData from "@/hooks/useFetchData"; import { deleteAnnotationTaskByIdUsingDelete, queryAnnotationTasksUsingGet, } from "../annotation.api"; -import { mapAnnotationTask } from "../annotation.const"; +import { mapAnnotationTask, type AnnotationTaskListItem } from "../annotation.const"; import CreateAnnotationTask from "../Create/components/CreateAnnotationTaskDialog"; import ExportAnnotationDialog from "./ExportAnnotationDialog"; import { ColumnType } from "antd/es/table"; import { TemplateList } from "../Template"; // 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() { // return ; const navigate = useNavigate(); const [activeTab, setActiveTab] = useState("tasks"); const [viewMode, setViewMode] = useState<"list" | "card">("list"); const [showCreateDialog, setShowCreateDialog] = useState(false); - const [exportTask, setExportTask] = useState(null); - const [editTask, setEditTask] = useState(null); + const [exportTask, setExportTask] = useState(null); + const [editTask, setEditTask] = useState(null); const { loading, @@ -40,13 +48,13 @@ export default function DataAnnotation() { fetchData, handleFiltersChange, handleKeywordChange, - } = useFetchData(queryAnnotationTasksUsingGet, mapAnnotationTask, 30000, true, [], 0); + } = useFetchData(queryAnnotationTasksUsingGet, mapAnnotationTask, 30000, true, [], 0); - const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]); - const [selectedRows, setSelectedRows] = useState([]); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); - const handleAnnotate = (task: AnnotationTask) => { - const projectId = (task as any)?.id; + const handleAnnotate = (task: AnnotationTaskListItem) => { + const projectId = task.id; if (!projectId) { message.error("无法进入标注:缺少标注项目ID"); return; @@ -54,15 +62,15 @@ export default function DataAnnotation() { navigate(`/data/annotation/annotate/${projectId}`); }; - const handleExport = (task: AnnotationTask) => { + const handleExport = (task: AnnotationTaskListItem) => { setExportTask(task); }; - const handleEdit = (task: AnnotationTask) => { + const handleEdit = (task: AnnotationTaskListItem) => { setEditTask(task); }; - const handleDelete = (task: AnnotationTask) => { + const handleDelete = (task: AnnotationTaskListItem) => { Modal.confirm({ title: `确认删除标注任务「${task.name}」吗?`, content: "删除标注任务不会删除对应数据集,但会删除该任务的所有标注结果。", @@ -110,7 +118,7 @@ export default function DataAnnotation() { }); }; - const operations = [ + const operations: AnnotationTaskOperation[] = [ { key: "annotate", label: "标注", @@ -142,7 +150,7 @@ export default function DataAnnotation() { }, ]; - const columns: ColumnType[] = [ + const columns: ColumnType[] = [ { title: "任务名称", dataIndex: "name", @@ -173,7 +181,7 @@ export default function DataAnnotation() { key: "annotatedCount", width: 100, align: "center" as const, - render: (value: number, record: any) => { + render: (value: number, record: AnnotationTaskListItem) => { const total = record.totalCount || 0; const annotated = value || 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: "创建时间", dataIndex: "createdAt", @@ -202,14 +227,14 @@ export default function DataAnnotation() { fixed: "right" as const, width: 150, dataIndex: "actions", - render: (_: any, task: any) => ( + render: (_value: unknown, task: AnnotationTaskListItem) => (
{operations.map((operation) => (
); -} \ No newline at end of file +} diff --git a/frontend/src/pages/DataAnnotation/annotation.const.tsx b/frontend/src/pages/DataAnnotation/annotation.const.tsx index 7310547..77b5ba9 100644 --- a/frontend/src/pages/DataAnnotation/annotation.const.tsx +++ b/frontend/src/pages/DataAnnotation/annotation.const.tsx @@ -6,6 +6,64 @@ import { CloseCircleOutlined, } 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 = { [AnnotationTaskStatus.ACTIVE]: { 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 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 ? [ @@ -45,6 +105,8 @@ export function mapAnnotationTask(task: any) { // provide consistent field for components labelingProjId, projId: labelingProjId, + segmentationEnabled, + inProgressCount, name: task.name, description: task.description || "", datasetName: task.datasetName || task.dataset_name || "-", @@ -478,4 +540,4 @@ export const TemplateTypeMap = { label: "自定义", value: TemplateType.CUSTOM }, -} \ No newline at end of file +} diff --git a/runtime/datamate-python/app/module/annotation/schema/mapping.py b/runtime/datamate-python/app/module/annotation/schema/mapping.py index f094c55..7e7ef6c 100644 --- a/runtime/datamate-python/app/module/annotation/schema/mapping.py +++ b/runtime/datamate-python/app/module/annotation/schema/mapping.py @@ -61,6 +61,7 @@ class DatasetMappingResponse(BaseModel): ) total_count: int = Field(0, alias="totalCount", 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="创建时间") updated_at: Optional[datetime] = Field(None, alias="updatedAt", description="更新时间") deleted_at: Optional[datetime] = Field(None, alias="deletedAt", description="删除时间") diff --git a/runtime/datamate-python/app/module/annotation/service/mapping.py b/runtime/datamate-python/app/module/annotation/service/mapping.py index 5df9ab1..9bf51e8 100644 --- a/runtime/datamate-python/app/module/annotation/service/mapping.py +++ b/runtime/datamate-python/app/module/annotation/service/mapping.py @@ -8,6 +8,7 @@ import uuid from app.core.logging import get_logger 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.module.annotation.schema import ( DatasetMappingCreateRequest, @@ -40,7 +41,7 @@ class DatasetMappingService: self, project_id: str, dataset_id: str - ) -> Tuple[int, int]: + ) -> Tuple[int, int, int]: """ 获取项目的统计数据 @@ -49,7 +50,7 @@ class DatasetMappingService: dataset_id: 数据集ID Returns: - (total_count, annotated_count) 元组 + (total_count, annotated_count, in_progress_count) 元组 """ # 获取标注项目快照数据量(只统计快照内的文件) total_result = await self.db.execute( @@ -71,7 +72,16 @@ class DatasetMappingService: ) 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( self, @@ -110,7 +120,7 @@ class DatasetMappingService: 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 ) @@ -127,6 +137,7 @@ class DatasetMappingService: "segmentation_enabled": segmentation_enabled, "total_count": total_count, "annotated_count": annotated_count, + "in_progress_count": in_progress_count, "created_at": mapping.created_at, "updated_at": mapping.updated_at, "deleted_at": mapping.deleted_at, @@ -177,9 +188,9 @@ class DatasetMappingService: 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: - 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 ) @@ -197,6 +208,7 @@ class DatasetMappingService: "segmentation_enabled": segmentation_enabled, "total_count": total_count, "annotated_count": annotated_count, + "in_progress_count": in_progress_count, "created_at": mapping.created_at, "updated_at": mapping.updated_at, "deleted_at": mapping.deleted_at,