diff --git a/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx b/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx index 0483edd..373ebc7 100644 --- a/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx +++ b/frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx @@ -1,16 +1,17 @@ -import { useState, useEffect } from "react"; -import { Card, Button, Table, message, Modal, Tabs, Tag, Progress, Tooltip } from "antd"; -import { - PlusOutlined, - EditOutlined, - DeleteOutlined, - SyncOutlined, -} from "@ant-design/icons"; -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 { useState, useEffect } from "react"; +import { Card, Button, Table, message, Modal, Tabs, Tag, Progress, Tooltip } from "antd"; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + SyncOutlined, + DownloadOutlined, +} from "@ant-design/icons"; +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, @@ -20,6 +21,7 @@ import { } from "../annotation.api"; import { mapAnnotationTask } 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 @@ -40,13 +42,14 @@ const AUTO_MODEL_SIZE_LABELS: Record = { x: "YOLOv8x (最精确)", }; -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 [autoTasks, setAutoTasks] = useState([]); +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 [autoTasks, setAutoTasks] = useState([]); const { loading, @@ -58,8 +61,8 @@ export default function DataAnnotation() { handleKeywordChange, } = useFetchData(queryAnnotationTasksUsingGet, mapAnnotationTask, 30000, true, [], 0); - const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]); - const [selectedRows, setSelectedRows] = useState([]); + const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([]); + const [selectedRows, setSelectedRows] = useState([]); // 拉取自动标注任务(供轮询和创建成功后立即刷新复用) const refreshAutoTasks = async (silent = false) => { @@ -77,24 +80,28 @@ export default function DataAnnotation() { } }; - // 自动标注任务轮询(用于在同一表格中展示处理进度) - useEffect(() => { - refreshAutoTasks(); - const timer = setInterval(() => refreshAutoTasks(true), 3000); + // 自动标注任务轮询(用于在同一表格中展示处理进度) + useEffect(() => { + refreshAutoTasks(); + const timer = setInterval(() => refreshAutoTasks(true), 3000); return () => { clearInterval(timer); }; }, []); - const handleAnnotate = (task: AnnotationTask) => { - const projectId = (task as any)?.id; - if (!projectId) { - message.error("无法进入标注:缺少标注项目ID"); - return; - } - navigate(`/data/annotation/annotate/${projectId}`); - }; + const handleAnnotate = (task: AnnotationTask) => { + const projectId = (task as any)?.id; + if (!projectId) { + message.error("无法进入标注:缺少标注项目ID"); + return; + } + navigate(`/data/annotation/annotate/${projectId}`); + }; + + const handleExport = (task: AnnotationTask) => { + setExportTask(task); + }; const handleDelete = (task: AnnotationTask) => { Modal.confirm({ @@ -257,6 +264,12 @@ export default function DataAnnotation() { ), onClick: handleAnnotate, }, + { + key: "export", + label: "导出", + icon: , + onClick: handleExport, + }, { key: "sync", label: "同步", @@ -552,6 +565,13 @@ export default function DataAnnotation() { } }} /> + + setExportTask(null)} + /> ), }, diff --git a/frontend/src/pages/DataAnnotation/Home/ExportAnnotationDialog.tsx b/frontend/src/pages/DataAnnotation/Home/ExportAnnotationDialog.tsx new file mode 100644 index 0000000..8286ba6 --- /dev/null +++ b/frontend/src/pages/DataAnnotation/Home/ExportAnnotationDialog.tsx @@ -0,0 +1,219 @@ +import { useState, useEffect } from "react"; +import { + Modal, + Form, + Select, + Checkbox, + Spin, + Statistic, + Row, + Col, + message, + Alert, +} from "antd"; +import { FileTextOutlined, CheckCircleOutlined } from "@ant-design/icons"; +import { + getExportStatsUsingGet, + downloadAnnotationsUsingGet, + ExportFormat, +} from "../annotation.api"; + +interface ExportAnnotationDialogProps { + open: boolean; + projectId: string; + projectName: string; + onClose: () => void; +} + +const FORMAT_OPTIONS: { label: string; value: ExportFormat; description: string }[] = [ + { + label: "JSON", + value: "json", + description: "Label Studio 原生格式,包含完整标注结构", + }, + { + label: "JSON Lines", + value: "jsonl", + description: "每行一条记录,适合大数据处理", + }, + { + label: "CSV", + value: "csv", + description: "表格格式,可用 Excel 打开", + }, + { + label: "COCO", + value: "coco", + description: "目标检测通用格式,适用于图像标注", + }, + { + label: "YOLO", + value: "yolo", + description: "YOLO 格式(ZIP),适用于目标检测训练", + }, +]; + +export default function ExportAnnotationDialog({ + open, + projectId, + projectName, + onClose, +}: ExportAnnotationDialogProps) { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [exporting, setExporting] = useState(false); + const [stats, setStats] = useState<{ + totalFiles: number; + annotatedFiles: number; + } | null>(null); + + // 加载导出统计信息 + useEffect(() => { + if (open && projectId) { + setLoading(true); + getExportStatsUsingGet(projectId) + .then((res: any) => { + const data = res?.data || res; + setStats({ + totalFiles: data?.totalFiles || 0, + annotatedFiles: data?.annotatedFiles || 0, + }); + }) + .catch((err) => { + console.error("Failed to get export stats:", err); + message.error("获取导出统计失败"); + }) + .finally(() => { + setLoading(false); + }); + } + }, [open, projectId]); + + // 重置表单 + useEffect(() => { + if (open) { + form.setFieldsValue({ + format: "json", + onlyAnnotated: true, + includeData: false, + }); + } + }, [open, form]); + + const handleExport = async () => { + try { + const values = await form.validateFields(); + setExporting(true); + + const blob = await downloadAnnotationsUsingGet( + projectId, + values.format, + values.onlyAnnotated, + values.includeData + ); + + // 获取文件名 + const formatExt: Record = { + json: "json", + jsonl: "jsonl", + csv: "csv", + coco: "json", + yolo: "zip", + }; + const ext = formatExt[values.format as ExportFormat] || "json"; + const filename = `${projectName}_annotations.${ext}`; + + // 下载文件 + const url = window.URL.createObjectURL(blob as Blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + message.success("导出成功"); + onClose(); + } catch (err: any) { + console.error("Export failed:", err); + message.error(err?.message || "导出失败,请稍后重试"); + } finally { + setExporting(false); + } + }; + + return ( + + + {/* 统计信息 */} +
+ + + } + /> + + + } + /> + + +
+ + {/* 导出选项 */} +
+ +