From d84152b45fa0908def42d82e45c68f1c28653e90 Mon Sep 17 00:00:00 2001 From: chenghh-9609 <55340429+chenghh-9609@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:39:06 +0800 Subject: [PATCH] update data synthesis page ui (#60) * feat: Update site name to DataMate and refine text for AI data processing * feat: Refactor settings page and implement model access functionality - Created a new ModelAccess component for managing model configurations. - Removed the old Settings component and replaced it with a new SettingsPage component that integrates ModelAccess, SystemConfig, and WebhookConfig. - Added SystemConfig component for managing system settings. - Implemented WebhookConfig component for managing webhook configurations. - Updated API functions for model management in settings.apis.ts. - Adjusted routing to point to the new SettingsPage component. * feat: Implement Data Collection Page with Task Management and Execution Log - Created DataCollectionPage component to manage data collection tasks. - Added TaskManagement and ExecutionLog components for task handling and logging. - Integrated task operations including start, stop, edit, and delete functionalities. - Implemented filtering and searching capabilities in task management. - Introduced SimpleCronScheduler for scheduling tasks with cron expressions. - Updated CreateTask component to utilize new scheduling and template features. - Enhanced BasicInformation component to conditionally render fields based on visibility settings. - Refactored ImportConfiguration component to remove NAS import section. * feat: Update task creation API endpoint and enhance task creation form with new fields and validation * Refactor file upload and operator management components - Removed unnecessary console logs from file download and export functions. - Added size property to TaskItem interface for better task management. - Simplified TaskUpload component by utilizing useFileSliceUpload hook for file upload logic. - Enhanced OperatorPluginCreate component to handle file uploads and parsing more efficiently. - Updated ConfigureStep component to use Ant Design Form for better data handling and validation. - Improved PreviewStep component to navigate back to the operator market. - Added support for additional file types in UploadStep component. - Implemented delete operator functionality in OperatorMarketPage with confirmation prompts. - Cleaned up unused API functions in operator.api.ts to streamline the codebase. - Fixed number formatting utility to handle zero values correctly. * Refactor Knowledge Generation to Knowledge Base - Created new API service for Knowledge Base operations including querying, creating, updating, and deleting knowledge bases and files. - Added constants for Knowledge Base status and type mappings. - Defined models for Knowledge Base and related files. - Removed obsolete Knowledge Base creation and home components, replacing them with new implementations under the Knowledge Base structure. - Updated routing to reflect the new Knowledge Base paths. - Adjusted menu items to align with the new Knowledge Base terminology. - Modified ModelAccess interface to include modelName and type properties. * feat: Implement Knowledge Base Page with CRUD operations and data management - Added KnowledgeBasePage component for displaying and managing knowledge bases. - Integrated search and filter functionalities with SearchControls component. - Implemented CreateKnowledgeBase component for creating and editing knowledge bases. - Enhanced AddDataDialog for file uploads and dataset selections. - Introduced TableTransfer component for managing data transfers between tables. - Updated API functions for knowledge base operations, including file management. - Refactored knowledge base model to include file status and metadata. - Adjusted routing to point to the new KnowledgeBasePage. * feat: enhance OperatorPluginCreate and ConfigureStep for better upload handling and UI updates * refactor: remove unused components and clean up API logging in KnowledgeBase * feat: update icons in various components and improve styling for better UI consistency * fix: adjust upload step handling and improve error display in configuration step * feat: Add RatioTransfer component for dataset selection and configuration - Implemented RatioTransfer component to manage dataset selection and ratio configuration. - Integrated dataset fetching with search and filter capabilities. - Added RatioConfig component for displaying and updating selected datasets' configurations. - Enhanced SelectDataset component with improved UI and functionality for dataset selection. - Updated RatioTasksPage to utilize new ratio task status mapping and improved error handling for task deletion. - Refactored ratio model and constants for better type safety and clarity. - Changed Vite configuration to use local backend service for development. --- frontend/src/index.css | 12 +- frontend/src/mock/ratio.tsx | 193 ---------- .../RatioTask/Create/CreateRatioTask.tsx | 235 ++++++------ .../Create/components/BasicInformation.tsx | 6 +- .../Create/components/RatioConfig.tsx | 354 ++++++++++++++---- .../Create/components/RatioTransfer.tsx | 169 +++++++++ .../Create/components/SelectDataset.tsx | 143 ++++--- .../src/pages/RatioTask/Home/RatioTask.tsx | 267 ++++++------- frontend/src/pages/RatioTask/ratio.const.tsx | 46 +++ frontend/src/pages/RatioTask/ratio.model.ts | 99 ++--- frontend/vite.config.ts | 3 +- 11 files changed, 857 insertions(+), 670 deletions(-) delete mode 100644 frontend/src/mock/ratio.tsx create mode 100644 frontend/src/pages/RatioTask/Create/components/RatioTransfer.tsx create mode 100644 frontend/src/pages/RatioTask/ratio.const.tsx diff --git a/frontend/src/index.css b/frontend/src/index.css index e5fca1b..172fcbb 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -58,12 +58,18 @@ @apply border border-[#f0f0f0] rounded-lg bg-white; } .border { - @apply border border-gray-100; + @apply border border-[#f0f0f0]; } .border-bottom { - @apply border-b border-gray-100; + @apply border-b border-[#f0f0f0]; } .border-top { - @apply border-t border-gray-100; + @apply border-t border-[#f0f0f0]; + } + .border-right { + @apply border-r border-[#f0f0f0]; + } + .border-left { + @apply border-l border-[#f0f0f0]; } } diff --git a/frontend/src/mock/ratio.tsx b/frontend/src/mock/ratio.tsx deleted file mode 100644 index 71d8b3e..0000000 --- a/frontend/src/mock/ratio.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import type { RatioTask } from "@/pages/RatioTask/ratio.model.ts"; - -export const mockRatioTasks: RatioTask[] = [ - { - id: 1, - name: "多领域数据配比任务", - status: "completed", - progress: 100, - sourceDatasets: [ - "orig_20250724_64082", - "financial_qa_dataset", - "medical_corpus", - ], - targetCount: 10000, - generatedCount: 10000, - createdAt: "2025-01-24", - ratioType: "dataset", - estimatedTime: "已完成", - quality: 94, - ratioConfigs: [ - { - id: "1", - name: "通用文本", - type: "dataset", - quantity: 4000, - percentage: 40, - source: "orig_20250724_64082", - }, - { - id: "2", - name: "金融问答", - type: "dataset", - quantity: 3000, - percentage: 30, - source: "financial_qa_dataset", - }, - { - id: "3", - name: "医疗语料", - type: "dataset", - quantity: 3000, - percentage: 30, - source: "medical_corpus", - }, - ], - }, - { - id: 2, - name: "标签配比训练集", - status: "running", - progress: 68, - sourceDatasets: ["teacher_model_outputs", "image_text_pairs"], - targetCount: 8000, - generatedCount: 5440, - createdAt: "2025-01-25", - ratioType: "label", - estimatedTime: "剩余 12 分钟", - quality: 89, - ratioConfigs: [ - { - id: "1", - name: "问答", - type: "label", - quantity: 2500, - percentage: 31.25, - source: "teacher_model_outputs_问答", - }, - { - id: "2", - name: "推理", - type: "label", - quantity: 2000, - percentage: 25, - source: "teacher_model_outputs_推理", - }, - { - id: "3", - name: "图像", - type: "label", - quantity: 1800, - percentage: 22.5, - source: "image_text_pairs_图像", - }, - { - id: "4", - name: "描述", - type: "label", - quantity: 1700, - percentage: 21.25, - source: "image_text_pairs_描述", - }, - ], - }, - { - id: 3, - name: "平衡数据集配比", - status: "failed", - progress: 25, - sourceDatasets: ["orig_20250724_64082", "financial_qa_dataset"], - targetCount: 5000, - generatedCount: 1250, - createdAt: "2025-01-25", - ratioType: "dataset", - errorMessage: "数据源连接失败,请检查数据集状态", - ratioConfigs: [ - { - id: "1", - name: "通用文本", - type: "dataset", - quantity: 2500, - percentage: 50, - source: "orig_20250724_64082", - }, - { - id: "2", - name: "金融问答", - type: "dataset", - quantity: 2500, - percentage: 50, - source: "financial_qa_dataset", - }, - ], - }, - { - id: 4, - name: "文本分类配比任务", - status: "pending", - progress: 0, - sourceDatasets: ["text_classification_data", "sentiment_analysis_data"], - targetCount: 6000, - generatedCount: 0, - createdAt: "2025-01-26", - ratioType: "label", - estimatedTime: "预计 15 分钟", - ratioConfigs: [ - { - id: "1", - name: "正面", - type: "label", - quantity: 2000, - percentage: 33.33, - source: "sentiment_analysis_data_正面", - }, - { - id: "2", - name: "负面", - type: "label", - quantity: 2000, - percentage: 33.33, - source: "sentiment_analysis_data_负面", - }, - { - id: "3", - name: "中性", - type: "label", - quantity: 2000, - percentage: 33.33, - source: "sentiment_analysis_data_中性", - }, - ], - }, - { - id: 5, - name: "多模态数据配比", - status: "paused", - progress: 45, - sourceDatasets: ["image_caption_data", "video_description_data"], - targetCount: 12000, - generatedCount: 5400, - createdAt: "2025-01-23", - ratioType: "dataset", - estimatedTime: "已暂停", - quality: 91, - ratioConfigs: [ - { - id: "1", - name: "图像描述", - type: "dataset", - quantity: 7000, - percentage: 58.33, - source: "image_caption_data", - }, - { - id: "2", - name: "视频描述", - type: "dataset", - quantity: 5000, - percentage: 41.67, - source: "video_description_data", - }, - ], - }, -]; diff --git a/frontend/src/pages/RatioTask/Create/CreateRatioTask.tsx b/frontend/src/pages/RatioTask/Create/CreateRatioTask.tsx index ece5ada..7d0d0ef 100644 --- a/frontend/src/pages/RatioTask/Create/CreateRatioTask.tsx +++ b/frontend/src/pages/RatioTask/Create/CreateRatioTask.tsx @@ -1,15 +1,15 @@ -import { useState } from "react"; -import { Button, Card, Form, Divider, message } from "antd"; -import { ArrowLeft, Play, BarChart3, Shuffle, PieChart } from "lucide-react"; +import { useMemo, useState } from "react"; +import { Button, Form, message } from "antd"; +import { ArrowLeft, ChevronRight } from "lucide-react"; import { createRatioTaskUsingPost } from "@/pages/RatioTask/ratio.api.ts"; import type { Dataset } from "@/pages/DataManagement/dataset.model.ts"; import { useNavigate } from "react-router"; import SelectDataset from "@/pages/RatioTask/Create/components/SelectDataset.tsx"; import BasicInformation from "@/pages/RatioTask/Create/components/BasicInformation.tsx"; import RatioConfig from "@/pages/RatioTask/Create/components/RatioConfig.tsx"; +import RatioTransfer from "./components/RatioTransfer"; export default function CreateRatioTask() { - const navigate = useNavigate(); const [form] = Form.useForm(); // 配比任务相关状态 @@ -25,8 +25,9 @@ export default function CreateRatioTask() { const [datasets, setDatasets] = useState([]); const [creating, setCreating] = useState(false); - const [distributions, setDistributions] = useState>>({}); - + const [distributions, setDistributions] = useState< + Record> + >({}); const handleCreateRatioTask = async () => { try { @@ -36,7 +37,8 @@ export default function CreateRatioTask() { return; } // Build request payload - const ratio_method = ratioTaskForm.ratioType === "dataset" ? "DATASET" : "TAG"; + const ratio_method = + ratioTaskForm.ratioType === "dataset" ? "DATASET" : "TAG"; const totals = String(values.totalTargetCount); const config = ratioTaskForm.ratioConfigs.map((c) => { if (ratio_method === "DATASET") { @@ -69,11 +71,19 @@ export default function CreateRatioTask() { message.success("配比任务创建成功"); navigate("/data/synthesis/ratio-task"); } catch { - // 校验失败 + message.error("配比任务创建失败,请重试"); } finally { setCreating(false); } }; + const totalConfigured = useMemo( + () => + ratioTaskForm?.ratioConfigs?.reduce?.( + (sum, c) => sum + (c.quantity || 0), + 0 + ) || 0, + [ratioTaskForm.ratioConfigs] + ); // dataset selection is handled inside SelectDataset via onSelectedDatasetsChange @@ -137,10 +147,16 @@ export default function CreateRatioTask() { }; // 标签模式下,更新某数据集的某个标签的数量 - const updateLabelRatioConfig = (datasetId: string, label: string, quantity: number) => { + const updateLabelRatioConfig = ( + datasetId: string, + label: string, + quantity: number + ) => { const sourceKey = `${datasetId}_${label}`; setRatioTaskForm((prev) => { - const existingIndex = prev.ratioConfigs.findIndex((c) => c.source === sourceKey); + const existingIndex = prev.ratioConfigs.findIndex( + (c) => c.source === sourceKey + ); const totalOtherQuantity = prev.ratioConfigs .filter((c) => c.source !== sourceKey) .reduce((sum, c) => sum + c.quantity, 0); @@ -176,9 +192,9 @@ export default function CreateRatioTask() { }; return ( -
+
{/* Header */} -
+
- -
-
- {/* 左侧:数据集选择 */} - setRatioTaskForm({ ...ratioTaskForm, ratioType: value, ratioConfigs: [] })} - onSelectedDatasetsChange={(next) => { - setRatioTaskForm((prev) => ({ - ...prev, - selectedDatasets: next, - ratioConfigs: prev.ratioConfigs.filter((c) => { - const id = String(c.source); - // keep only items whose dataset id remains selected - const dsId = id.includes("_") ? id.split("_")[0] : id; - return next.includes(dsId); - }), - })); - }} - onDistributionsChange={(next) => setDistributions(next)} - onDatasetsChange={(list) => setDatasets(list)} +
+
+ + - {/* 右侧:配比配置 */} -
-

- - 配比配置 -

- -
-
- - - 配比设置 - -
- 设置每个数据集的配比数量 -
-
- -
- - updateRatioConfig(datasetId, quantity)} - onUpdateLabelQuantity={(datasetId, label, quantity) => updateLabelRatioConfig(datasetId, label, quantity)} - /> - {/* 配比预览 */} - {ratioTaskForm.ratioConfigs.length > 0 && ( -
- 配比预览 -
-
-
- 总配比数量: - - {ratioTaskForm.ratioConfigs - .reduce((sum, config) => sum + config.quantity, 0) - .toLocaleString()} - -
-
- 目标数量: - - {ratioTaskForm.totalTargetCount.toLocaleString()} - -
-
- 配比项目: - - {ratioTaskForm.ratioConfigs.length}个 - -
-
-
-
- )} - -
- - -
-
+ + {/* */} + +
+ + setRatioTaskForm({ + ...ratioTaskForm, + ratioType: value, + ratioConfigs: [], + }) + } + onSelectedDatasetsChange={(next) => { + setRatioTaskForm((prev) => ({ + ...prev, + selectedDatasets: next, + ratioConfigs: prev.ratioConfigs.filter((c) => { + const id = String(c.source); + // keep only items whose dataset id remains selected + const dsId = id.includes("_") ? id.split("_")[0] : id; + return next.includes(dsId); + }), + })); + }} + onDistributionsChange={(next) => setDistributions(next)} + onDatasetsChange={(list) => setDatasets(list)} + /> + + + setRatioTaskForm((prev) => ({ + ...prev, + ratioConfigs: configs, + })) + } + />
-
- - + +
+
+ + +
+
); } diff --git a/frontend/src/pages/RatioTask/Create/components/BasicInformation.tsx b/frontend/src/pages/RatioTask/Create/components/BasicInformation.tsx index 67a3c5a..e6f6499 100644 --- a/frontend/src/pages/RatioTask/Create/components/BasicInformation.tsx +++ b/frontend/src/pages/RatioTask/Create/components/BasicInformation.tsx @@ -7,9 +7,11 @@ interface BasicInformationProps { totalTargetCount: number; } -const BasicInformation: React.FC = ({ totalTargetCount }) => { +const BasicInformation: React.FC = ({ + totalTargetCount, +}) => { return ( -
+
>; - onUpdateDatasetQuantity: (datasetId: string, quantity: number) => void; - onUpdateLabelQuantity: (datasetId: string, label: string, quantity: number) => void; + onChange?: (configs: RatioConfigItem[]) => void; } const RatioConfig: React.FC = ({ ratioType, selectedDatasets, datasets, - ratioConfigs, totalTargetCount, distributions, - onUpdateDatasetQuantity, - onUpdateLabelQuantity, + onChange, }) => { - const totalConfigured = ratioConfigs.reduce((sum, c) => sum + (c.quantity || 0), 0); + const [ratioConfigs, setRatioConfigs] = useState([]); + + // 配比项总数 + const totalConfigured = useMemo( + () => ratioConfigs.reduce((sum, c) => sum + (c.quantity || 0), 0), + [ratioConfigs] + ); + + // 更新数据集配比项 + const updateDatasetQuantity = (datasetId: string, quantity: number) => { + setRatioConfigs((prev) => { + const existingIndex = prev.findIndex( + (config) => config.source === datasetId + ); + const totalOtherQuantity = prev + .filter((config) => config.source !== datasetId) + .reduce((sum, config) => sum + config.quantity, 0); + + const dataset = datasets.find((d) => String(d.id) === datasetId); + const newConfig: RatioConfigItem = { + id: datasetId, + name: dataset?.name || datasetId, + type: ratioType, + quantity: Math.min(quantity, totalTargetCount - totalOtherQuantity), + percentage: Math.round((quantity / totalTargetCount) * 100), + source: datasetId, + }; + + let newConfigs; + if (existingIndex >= 0) { + newConfigs = [...prev]; + newConfigs[existingIndex] = newConfig; + } else { + newConfigs = [...prev, newConfig]; + } + onChange?.(newConfigs); + return newConfigs; + }); + }; + + // 自动平均分配 + const generateAutoRatio = () => { + const selectedCount = selectedDatasets.length; + if (selectedCount === 0) return; + const baseQuantity = Math.floor(totalTargetCount / selectedCount); + const remainder = totalTargetCount % selectedCount; + const newConfigs = selectedDatasets.map((datasetId, index) => { + const dataset = datasets.find((d) => String(d.id) === datasetId); + const quantity = baseQuantity + (index < remainder ? 1 : 0); + return { + id: datasetId, + name: dataset?.name || datasetId, + type: ratioType, + quantity, + percentage: Math.round((quantity / totalTargetCount) * 100), + source: datasetId, + }; + }); + setRatioConfigs(newConfigs); + onChange?.(newConfigs); + }; + + // 标签模式下,更新某数据集的某个标签的数量 + const updateLabelQuantity = ( + datasetId: string, + label: string, + quantity: number + ) => { + const sourceKey = `${datasetId}_${label}`; + setRatioConfigs((prev) => { + const existingIndex = prev.findIndex((c) => c.source === sourceKey); + const totalOtherQuantity = prev + .filter((c) => c.source !== sourceKey) + .reduce((sum, c) => sum + c.quantity, 0); + const dist = distributions[datasetId] || {}; + const labelMax = dist[label] ?? Infinity; + const cappedQuantity = Math.max( + 0, + Math.min(quantity, totalTargetCount - totalOtherQuantity, labelMax) + ); + const newConfig: RatioConfigItem = { + id: sourceKey, + name: label, + type: "label", + quantity: cappedQuantity, + percentage: Math.round((cappedQuantity / totalTargetCount) * 100), + source: sourceKey, + }; + let newConfigs; + if (existingIndex >= 0) { + newConfigs = [...prev]; + newConfigs[existingIndex] = newConfig; + } else { + newConfigs = [...prev, newConfig]; + } + onChange?.(newConfigs); + return newConfigs; + }); + }; + + // 选中数据集变化时,移除未选中的配比项 + React.useEffect(() => { + setRatioConfigs((prev) => { + const next = prev.filter((c) => { + const id = String(c.source); + const dsId = id.includes("_") ? id.split("_")[0] : id; + return selectedDatasets.includes(dsId); + }); + if (next !== prev) onChange?.(next); + return next; + }); + // eslint-disable-next-line + }, [selectedDatasets]); return ( -
-
- 配比设置 - - 已配置: {totalConfigured} / {totalTargetCount} +
+
+ + 配比配置 + + (已配置:{totalConfigured}/{totalTargetCount}条) + +
{selectedDatasets.length === 0 ? (
@@ -49,80 +167,150 @@ const RatioConfig: React.FC = ({

请先选择数据集

) : ( -
- {selectedDatasets.map((datasetId) => { - const dataset = datasets.find((d) => String(d.id) === datasetId); - const config = ratioConfigs.find((c) => c.source === datasetId); - const currentQuantity = config?.quantity || 0; - if (!dataset) return null; - return ( - -
-
- {dataset.name} - {dataset.fileCount}条 +
+ {/* 配比预览 */} + {ratioConfigs.length > 0 && ( +
+
+
+
+ 总配比数量: + + {ratioConfigs + .reduce((sum, config) => sum + config.quantity, 0) + .toLocaleString()} + +
+
+ 目标数量: + + {totalTargetCount.toLocaleString()} + +
+
+ 配比项目: + + {ratioConfigs.length}个 +
-
{config?.percentage || 0}%
- {ratioType === "dataset" ? ( -
-
- 数量: - onUpdateDatasetQuantity(datasetId, Number(e.target.value))} - style={{ width: 80 }} - min={0} - max={Math.min(dataset.fileCount || 0, totalTargetCount)} - /> - +
+
+ )} +
+ {selectedDatasets.map((datasetId) => { + const dataset = datasets.find((d) => String(d.id) === datasetId); + const config = ratioConfigs.find((c) => c.source === datasetId); + const currentQuantity = config?.quantity || 0; + if (!dataset) return null; + return ( + +
+
+ + {dataset.name} + + {dataset.fileCount}条 +
+
+ {config?.percentage || 0}%
-
- ) : ( -
- {!distributions[String(dataset.id)] ? ( -
加载标签分布...
- ) : Object.entries(distributions[String(dataset.id)]).length === 0 ? ( -
该数据集暂无标签
- ) : ( -
- {Object.entries(distributions[String(dataset.id)]).map(([label, count]) => { - const sourceKey = `${datasetId}_${label}`; - const labelConfig = ratioConfigs.find((c) => c.source === sourceKey); - const labelQuantity = labelConfig?.quantity || 0; - return ( -
-
- {label} - {count}条 -
-
- 数量: - onUpdateLabelQuantity(datasetId, label, Number(e.target.value))} - style={{ width: 80 }} - min={0} - max={Math.min(Number(count) || 0, totalTargetCount)} - /> - -
-
- ); - })} + {ratioType === "dataset" ? ( +
+
+ 数量: + + updateDatasetQuantity( + datasetId, + Number(e.target.value) + ) + } + style={{ width: 80 }} + min={0} + max={Math.min( + dataset.fileCount || 0, + totalTargetCount + )} + /> +
- )} -
- )} - - ); - })} + +
+ ) : ( +
+ {!distributions[String(dataset.id)] ? ( +
+ 加载标签分布... +
+ ) : Object.entries(distributions[String(dataset.id)]) + .length === 0 ? ( +
+ 该数据集暂无标签 +
+ ) : ( +
+ {Object.entries( + distributions[String(dataset.id)] + ).map(([label, count]) => { + const sourceKey = `${datasetId}_${label}`; + const labelConfig = ratioConfigs.find( + (c) => c.source === sourceKey + ); + const labelQuantity = labelConfig?.quantity || 0; + return ( +
+
+ {label} + + {count}条 + +
+
+ 数量: + + updateLabelQuantity( + datasetId, + label, + Number(e.target.value) + ) + } + style={{ width: 80 }} + min={0} + max={Math.min( + Number(count) || 0, + totalTargetCount + )} + /> + + 条 + +
+
+ ); + })} +
+ )} +
+ )} + + ); + })} +
)}
diff --git a/frontend/src/pages/RatioTask/Create/components/RatioTransfer.tsx b/frontend/src/pages/RatioTask/Create/components/RatioTransfer.tsx new file mode 100644 index 0000000..d182725 --- /dev/null +++ b/frontend/src/pages/RatioTask/Create/components/RatioTransfer.tsx @@ -0,0 +1,169 @@ +import React, { useMemo } from "react"; +import { Table } from "antd"; +import { TransferItem } from "antd/es/transfer"; +import RatioConfig from "./RatioConfig"; +import useFetchData from "@/hooks/useFetchData"; +import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api"; +import { + datasetTypeMap, + mapDataset, +} from "@/pages/DataManagement/dataset.const"; +import { SearchControls } from "@/components/SearchControls"; + +const leftColumns = [ + { + dataIndex: "name", + title: "名称", + ellipsis: true, + }, + { + dataIndex: "datasetType", + title: "类型", + ellipsis: true, + width: 100, + render: (type: string) => datasetTypeMap[type].label, + }, + { + dataIndex: "size", + title: "大小", + width: 100, + ellipsis: true, + }, +]; + +export default function RatioTransfer(props: { + distributions: Record>; + ratioTaskForm: any; + updateRatioConfig: (datasetId: string, quantity: number) => void; + updateLabelRatioConfig: ( + datasetId: string, + label: string, + quantity: number + ) => void; +}) { + const { + updateLabelRatioConfig, + updateRatioConfig, + ratioTaskForm, + distributions, + } = props; + const { + tableData: datasets, + loading, + pagination, + searchParams, + setSearchParams, + handleFiltersChange, + } = useFetchData(queryDatasetsUsingGet, mapDataset); + + const [selectedDatasets, setSelectedDatasets] = React.useState< + TransferItem[] + >([]); + + const selectedRowKeys = useMemo(() => { + return selectedDatasets.map((item) => item.key); + }, [selectedDatasets]); + + const [listDisabled, setListDisabled] = React.useState(false); + + const generateAutoRatio = () => { + const selectedCount = ratioTaskForm.selectedDatasets.length; + if (selectedCount === 0) return; + + const baseQuantity = Math.floor( + ratioTaskForm.totalTargetCount / selectedCount + ); + const remainder = ratioTaskForm.totalTargetCount % selectedCount; + + const newConfigs = ratioTaskForm.selectedDatasets.map( + (datasetId, index) => { + const quantity = baseQuantity + (index < remainder ? 1 : 0); + return { + id: datasetId, + name: datasetId, + type: ratioTaskForm.ratioType, + quantity, + percentage: Math.round( + (quantity / ratioTaskForm.totalTargetCount) * 100 + ), + source: datasetId, + }; + } + ); + setRatioTaskForm((prev) => ({ ...prev, ratioConfigs: newConfigs })); + }; + + return ( +
+
+

{`${selectedDatasets.length} / ${datasets.length} 项`}

+ + setSearchParams({ ...searchParams, keyword }) + } + searchPlaceholder="搜索数据集名称..." + filters={[ + { + key: "type", + label: "数据集类型", + options: [ + { value: "dataset", label: "按数据集" }, + { value: "tag", label: "按标签" }, + ], + }, + ]} + onFiltersChange={handleFiltersChange} + onClearFilters={() => + setSearchParams({ ...searchParams, filter: {} }) + } + showViewToggle={false} + showReload={false} + className="m-4" + /> + { + setSelectedDatasets(selectedRows); + }, + selectedRowKeys, + selections: [ + Table.SELECTION_ALL, + Table.SELECTION_INVERT, + Table.SELECTION_NONE, + ], + }} + columns={leftColumns} + dataSource={datasets} + loading={loading} + pagination={pagination} + size="small" + rowKey="id" + style={{ pointerEvents: listDisabled ? "none" : undefined }} + onRow={(record) => ({ + onClick: () => { + if (record.disabled || listDisabled) { + return; + } + setSelectedDatasets((prev) => { + if (prev.includes(record.key)) { + return prev.filter((k) => k !== record.key); + } + return [...prev, record.key]; + }); + }, + })} + /> + +
+ +
+ + ); +} diff --git a/frontend/src/pages/RatioTask/Create/components/SelectDataset.tsx b/frontend/src/pages/RatioTask/Create/components/SelectDataset.tsx index ab74b8c..c99018e 100644 --- a/frontend/src/pages/RatioTask/Create/components/SelectDataset.tsx +++ b/frontend/src/pages/RatioTask/Create/components/SelectDataset.tsx @@ -1,15 +1,21 @@ import React, { useEffect, useState } from "react"; import { Badge, Button, Card, Checkbox, Input, Pagination, Select } from "antd"; -import { Database, Search as SearchIcon } from "lucide-react"; +import { Search as SearchIcon } from "lucide-react"; import type { Dataset } from "@/pages/DataManagement/dataset.model.ts"; -import { queryDatasetsUsingGet, queryDatasetByIdUsingGet, queryDatasetStatisticsByIdUsingGet } from "@/pages/DataManagement/dataset.api.ts"; +import { + queryDatasetsUsingGet, + queryDatasetByIdUsingGet, + queryDatasetStatisticsByIdUsingGet, +} from "@/pages/DataManagement/dataset.api.ts"; interface SelectDatasetProps { selectedDatasets: string[]; ratioType: "dataset" | "label"; onRatioTypeChange: (val: "dataset" | "label") => void; onSelectedDatasetsChange: (next: string[]) => void; - onDistributionsChange?: (next: Record>) => void; + onDistributionsChange?: ( + next: Record> + ) => void; onDatasetsChange?: (list: Dataset[]) => void; } @@ -25,7 +31,9 @@ const SelectDataset: React.FC = ({ const [loading, setLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [pagination, setPagination] = useState({ page: 1, size: 10, total: 0 }); - const [distributions, setDistributions] = useState>>({}); + const [distributions, setDistributions] = useState< + Record> + >({}); // Fetch dataset list useEffect(() => { @@ -40,7 +48,10 @@ const SelectDataset: React.FC = ({ const list = data?.content || data?.data || []; setDatasets(list); onDatasetsChange?.(list); - setPagination((prev) => ({ ...prev, total: data?.totalElements ?? data?.total ?? 0 })); + setPagination((prev) => ({ + ...prev, + total: data?.totalElements ?? data?.total ?? 0, + })); } finally { setLoading(false); } @@ -52,7 +63,9 @@ const SelectDataset: React.FC = ({ useEffect(() => { const fetchDistributions = async () => { if (ratioType !== "label" || !datasets?.length) return; - const idsToFetch = datasets.map((d) => String(d.id)).filter((id) => !distributions[id]); + const idsToFetch = datasets + .map((d) => String(d.id)) + .filter((id) => !distributions[id]); if (!idsToFetch.length) return; try { const results = await Promise.all( @@ -66,7 +79,9 @@ const SelectDataset: React.FC = ({ }) ); - const next: Record> = { ...distributions }; + const next: Record> = { + ...distributions, + }; for (const { id, stats } of results) { let dist: Record | undefined = undefined; if (stats) { @@ -77,13 +92,16 @@ const SelectDataset: React.FC = ({ (stats as any).labels, (stats as any).distribution, ]; - let picked = candidates.find((c) => c && (typeof c === "object" || Array.isArray(c))); + let picked = candidates.find( + (c) => c && (typeof c === "object" || Array.isArray(c)) + ); if (Array.isArray(picked)) { const obj: Record = {}; picked.forEach((it: any) => { const key = it?.label ?? it?.name ?? it?.tag ?? it?.key; const val = it?.count ?? it?.value ?? it?.num ?? it?.total; - if (key != null && typeof val === "number") obj[String(key)] = val; + if (key != null && typeof val === "number") + obj[String(key)] = val; }); dist = obj; } else if (picked && typeof picked === "object") { @@ -107,7 +125,8 @@ const SelectDataset: React.FC = ({ picked.forEach((it: any) => { const key = it?.label ?? it?.name ?? it?.tag ?? it?.key; const val = it?.count ?? it?.value ?? it?.num ?? it?.total; - if (key != null && typeof val === "number") obj[String(key)] = val; + if (key != null && typeof val === "number") + obj[String(key)] = val; }); dist = obj; } else if (picked && typeof picked === "object") { @@ -135,7 +154,9 @@ const SelectDataset: React.FC = ({ const next = Array.from(new Set([...selectedDatasets, datasetId])); onSelectedDatasetsChange(next); } else { - onSelectedDatasetsChange(selectedDatasets.filter((id) => id !== datasetId)); + onSelectedDatasetsChange( + selectedDatasets.filter((id) => id !== datasetId) + ); } }; @@ -144,36 +165,47 @@ const SelectDataset: React.FC = ({ }; return ( -
-

- - 数据集选择 -

- -
- 配比方式: - } - placeholder="搜索数据集" - value={searchQuery} - onChange={(e) => { - setSearchQuery(e.target.value); - setPagination((p) => ({ ...p, page: 1 })); - }} + +
+
+
+ 配比方式: + } + placeholder="搜索数据集" + value={searchQuery} + onChange={(e) => { + setSearchQuery(e.target.value); + setPagination((p) => ({ ...p, page: 1 })); + }} + /> +
{loading && ( -
正在加载数据集...
+
+ 正在加载数据集... +
)} {!loading && datasets.map((dataset) => { @@ -183,7 +215,9 @@ const SelectDataset: React.FC = ({ onToggleDataset(idStr, !checked)} >
@@ -193,10 +227,14 @@ const SelectDataset: React.FC = ({ />
- {dataset.name} + + {dataset.name} + {dataset.datasetType}
-
{dataset.description}
+
+ {dataset.description} +
{dataset.fileCount}条 {dataset.size} @@ -209,14 +247,21 @@ const SelectDataset: React.FC = ({ {Object.entries(distributions[idStr]) .slice(0, 8) .map(([tag, count]) => ( - {`${tag}: ${count}`} + {`${tag}: ${count}`} ))}
) : ( -
未检测到标签分布
+
+ 未检测到标签分布 +
) ) : ( -
加载标签分布...
+
+ 加载标签分布... +
)}
)} @@ -227,22 +272,20 @@ const SelectDataset: React.FC = ({ })}
- 已选择 {selectedDatasets.length} 个数据集
- setPagination((prev) => ({ ...prev, page: p, size: ps }))} + onChange={(p, ps) => + setPagination((prev) => ({ ...prev, page: p, size: ps })) + } />
-
+
); }; diff --git a/frontend/src/pages/RatioTask/Home/RatioTask.tsx b/frontend/src/pages/RatioTask/Home/RatioTask.tsx index e4d61cc..f0cea74 100644 --- a/frontend/src/pages/RatioTask/Home/RatioTask.tsx +++ b/frontend/src/pages/RatioTask/Home/RatioTask.tsx @@ -1,59 +1,67 @@ import { useState } from "react"; import { Button, Card, Table, Tooltip, App } from "antd"; -import { Plus, Clock, Play, CheckCircle, AlertCircle, Pause, BarChart3 } from "lucide-react"; +import { Plus } from "lucide-react"; import { DeleteOutlined } from "@ant-design/icons"; -import type { RatioTaskItem } from "@/pages/RatioTask/ratio.model.ts"; +import type { RatioTaskItem } from "@/pages/RatioTask/ratio.model"; import { useNavigate } from "react-router"; -import CardView from "@/components/CardView.tsx"; -import { SearchControls } from "@/components/SearchControls.tsx"; -import { queryRatioTasksUsingGet, deleteRatioTasksUsingDelete } from "@/pages/RatioTask/ratio.api.ts"; +import CardView from "@/components/CardView"; +import { SearchControls } from "@/components/SearchControls"; +import { + deleteRatioTasksUsingDelete, + queryRatioTasksUsingGet, +} from "../ratio.api"; import useFetchData from "@/hooks/useFetchData"; +import { mapRatioTask, ratioTaskStatusMap } from "../ratio.const"; export default function RatioTasksPage() { - const navigate = useNavigate(); - const [viewMode, setViewMode] = useState<"card" | "list">("card"); const { message } = App.useApp(); + const navigate = useNavigate(); + const [viewMode, setViewMode] = useState<"card" | "list">("list"); - const { loading, tableData, pagination, searchParams, setSearchParams, handleFiltersChange, fetchData } = - useFetchData(queryRatioTasksUsingGet, (d) => d as RatioTaskItem, 30000, true, [], 0); + const { + loading, + tableData, + pagination, + searchParams, + setSearchParams, + handleFiltersChange, + fetchData, + } = useFetchData( + queryRatioTasksUsingGet, + mapRatioTask, + 30000, + true, + [], + 0 + ); - const handleDelete = async (id: string) => { - await deleteRatioTasksUsingDelete([id]); - message.success("删除成功"); - await fetchData(); + const handleDeleteTask = async (task: RatioTaskItem) => { + try { + // 调用删除接口 + await deleteRatioTasksUsingDelete(task.id); + message.success("任务删除成功"); + // 重新加载数据 + fetchData(); + } catch (error) { + message.error("任务删除失败,请稍后重试"); + } }; - const getStatusBadge = (status: string) => { - const s = (status || "").toUpperCase(); - const statusConfig = { - PENDING: { - label: "等待中", - color: "#f09e10ff", - icon: , - }, - RUNNING: { - label: "运行中", - color: "#007bff", - icon: , - }, - SUCCESS: { - label: "已完成", - color: "#28a745", - icon: , - }, - FAILED: { - label: "失败", - color: "#dc3545", - icon: , - }, - PAUSED: { - label: "已暂停", - color: "#6c757d", - icon: , - }, - }; - return statusConfig[s as keyof typeof statusConfig] || statusConfig.PENDING; - }; + // 搜索、筛选和视图控制相关 + const filters = [ + { + key: "status", + label: "状态筛选", + options: [ + { label: "全部状态", value: "all" }, + { label: "等待中", value: "PENDING" }, + { label: "运行中", value: "RUNNING" }, + { label: "已完成", value: "SUCCESS" }, + { label: "失败", value: "FAILED" }, + { label: "已暂停", value: "PAUSED" }, + ], + }, + ]; const columns = [ { @@ -61,14 +69,20 @@ export default function RatioTasksPage() { dataIndex: "name", key: "name", render: (text: string, record: RatioTaskItem) => ( - navigate(`/data/synthesis/ratio-task/detail/${record.id}`)}>{text} + + navigate(`/data/synthesis/ratio-task/detail/${record.id}`) + } + > + {text} + ), }, { title: "状态", dataIndex: "status", key: "status", - render: (v: string) => getStatusBadge(v).label, + render: (status) => ratioTaskStatusMap[status]?.label, }, { title: "配比方式", @@ -85,7 +99,13 @@ export default function RatioTasksPage() { dataIndex: "target_dataset_name", key: "target_dataset_name", render: (text: string, task: RatioTaskItem) => ( - navigate(`/data/management/detail/${task.target_dataset_id}`)}>{text} + + navigate(`/data/management/detail/${task.target_dataset_id}`) + } + > + {text} + ), }, { @@ -112,127 +132,25 @@ export default function RatioTasksPage() { }, ]; - const renderTableView = () => ( - -
- -

- 暂无配比任务 -

-

- {searchParams.keyword || (searchParams.filter?.status?.[0] && searchParams.filter?.status?.[0] !== "all") - ? "没有找到匹配的任务" - : "开始创建您的第一个配比任务"} -

- {!searchParams.keyword && (!searchParams.filter?.status?.length || searchParams.filter?.status?.[0] === "all") && ( - - )} - - ), - }} - /> - - ); const operations = [ { key: "delete", label: "删除", danger: true, confirm: { - title: "确认删除该数据集?", - description: "删除后该数据集将无法恢复,请谨慎操作。", + title: "确认删除该任务?", + description: "删除后该任务将无法恢复,请谨慎操作。", okText: "删除", cancelText: "取消", okType: "danger", }, icon: , - onClick: (item) => handleDelete(String(item.id)), - } - ]; - const renderCardView = () => ( - ({ - ...task, - description: task.ratio_method === "DATASET" ? "按数据集配比" : "按标签配比", - icon: , - iconColor: task.ratio_method === "DATASET" ? "bg-blue-100" : "bg-green-100", - statistics: [ - { - label: "目标数量", - value: (task.totals ?? 0).toLocaleString(), - }, - { - label: "目标数据集", - value: task.target_dataset_name ? ( - { - e.stopPropagation(); - navigate(`/data/management/detail/${task.target_dataset_id}`); - }}> - {task.target_dataset_name} - - ) : '无', - }, - { - label: "创建时间", - value: task.created_at || "-", - }, - ], - status: getStatusBadge(task.status), - }))} - pagination={pagination} - operations={operations} - onView={(task) => {navigate(`/data/synthesis/ratio-task/detail/${task.id}`)}} - /> - ); - - // 搜索、筛选和视图控制相关 - const searchFilters = [ - { - key: "status", - label: "状态筛选", - options: [ - { label: "全部状态", value: "all" }, - { label: "等待中", value: "PENDING" }, - { label: "运行中", value: "RUNNING" }, - { label: "已完成", value: "SUCCESS" }, - { label: "失败", value: "FAILED" }, - { label: "已暂停", value: "PAUSED" }, - ], + onClick: handleDeleteTask, }, ]; - // 处理 SearchControls 的筛选变化 - const handleSearchControlsFiltersChange = ( - filters: Record - ) => { - handleFiltersChange(filters); - }; - - // 处理视图切换 - const handleViewModeChange = (mode: "card" | "list") => { - setViewMode(mode === "card" ? "card" : "list"); - }; - return ( -
+

配比任务

+ + ) : ( + { + navigate(`/data/synthesis/ratio-task/detail/${task.id}`); + }} + /> + )} ); diff --git a/frontend/src/pages/RatioTask/ratio.const.tsx b/frontend/src/pages/RatioTask/ratio.const.tsx new file mode 100644 index 0000000..2177364 --- /dev/null +++ b/frontend/src/pages/RatioTask/ratio.const.tsx @@ -0,0 +1,46 @@ +import { formatDate } from "@/utils/unit"; +import { RatioTaskItem, RatioStatus } from "./ratio.model"; + +export const ratioTaskStatusMap: Record< + string, + { + value: RatioStatus; + label: string; + color: string; + } +> = { + [RatioStatus.PENDING]: { + value: RatioStatus.PENDING, + label: "等待中", + color: "blue", + }, + [RatioStatus.RUNNING]: { + value: RatioStatus.RUNNING, + label: "运行中", + color: "green", + }, + [RatioStatus.COMPLETED]: { + value: RatioStatus.COMPLETED, + label: "已完成", + color: "gray", + }, + [RatioStatus.FAILED]: { + value: RatioStatus.FAILED, + label: "失败", + color: "red", + }, + [RatioStatus.PAUSED]: { + value: RatioStatus.PAUSED, + label: "已暂停", + color: "orange", + }, +}; + +export function mapRatioTask(task: Partial): RatioTaskItem { + return { + ...task, + status: ratioTaskStatusMap[task.status || RatioStatus.PENDING]?.value, + createdAt: formatDate(task.created_at), + updatedAt: formatDate(task.updated_at), + }; +} diff --git a/frontend/src/pages/RatioTask/ratio.model.ts b/frontend/src/pages/RatioTask/ratio.model.ts index edfa60d..b0d4e36 100644 --- a/frontend/src/pages/RatioTask/ratio.model.ts +++ b/frontend/src/pages/RatioTask/ratio.model.ts @@ -1,71 +1,80 @@ // Ratio module models aligned with scripts/db/data-ratio-init.sql // enums -export type RatioMethod = "TAG" | "DATASET" -export type RatioStatus = "PENDING" | "RUNNING" | "COMPLETED" | "FAILED" | "PAUSED" +export type RatioMethod = "TAG" | "DATASET"; + +export enum RatioStatus { + PENDING = "PENDING", + RUNNING = "RUNNING", + COMPLETED = "COMPLETED", + FAILED = "FAILED", + PAUSED = "PAUSED", +} + +// interfaces // t_st_ratio_instances export interface RatioInstance { - id: string - name: string - description?: string - targetDatasetId?: string - ratioMethod?: RatioMethod - ratioParameters?: any - mergeMethod?: string - status?: RatioStatus | string - totals?: number - createdAt?: string - updatedAt?: string - createdBy?: string - updatedBy?: string + id: string; + name: string; + description?: string; + targetDatasetId?: string; + ratioMethod?: RatioMethod; + ratioParameters?: any; + mergeMethod?: string; + status?: RatioStatus | string; + totals?: number; + createdAt?: string; + updatedAt?: string; + createdBy?: string; + updatedBy?: string; } // t_st_ratio_relations export interface RatioRelation { - id: string - ratioInstanceId: string - sourceDatasetId?: string - ratioValue?: string - counts?: number - filterConditions?: string - createdAt?: string - updatedAt?: string - createdBy?: string - updatedBy?: string + id: string; + ratioInstanceId: string; + sourceDatasetId?: string; + ratioValue?: string; + counts?: number; + filterConditions?: string; + createdAt?: string; + updatedAt?: string; + createdBy?: string; + updatedBy?: string; } // API DTOs export interface RatioConfigItem { - datasetId: string - counts: string - filter_conditions: string + datasetId: string; + counts: string; + filter_conditions: string; } export interface CreateRatioTaskRequest { - name: string - description?: string - totals: string - ratio_method: RatioMethod - config: RatioConfigItem[] + name: string; + description?: string; + totals: string; + ratio_method: RatioMethod; + config: RatioConfigItem[]; } export interface TargetDatasetInfo { - id: string - name: string - datasetType: string - status: string + id: string; + name: string; + datasetType: string; + status: string; } export interface CreateRatioTaskResponse { - id: string - name: string - description?: string - totals: number - ratio_method: RatioMethod - status: string - config: RatioConfigItem[] - targetDataset: TargetDatasetInfo + id: string; + name: string; + description?: string; + totals: number; + ratio_method: RatioMethod; + status: string; + config: RatioConfigItem[]; + targetDataset: TargetDatasetInfo; } export interface RatioTaskItem { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 27d73c7..4ee6bc0 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -19,8 +19,7 @@ export default defineConfig({ // }, proxy: { "^/api": { - // target: "http://localhost:8002", // 本地后端服务地址 - target: "http://1.94.5.242:32530", // 远程后端服务地址 + target: "http://localhost:8002", // 本地后端服务地址 changeOrigin: true, secure: false, rewrite: (path) => path.replace(/^\/api/, "/api"),