You've already forked DataMate
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.
This commit is contained in:
@@ -58,12 +58,18 @@
|
|||||||
@apply border border-[#f0f0f0] rounded-lg bg-white;
|
@apply border border-[#f0f0f0] rounded-lg bg-white;
|
||||||
}
|
}
|
||||||
.border {
|
.border {
|
||||||
@apply border border-gray-100;
|
@apply border border-[#f0f0f0];
|
||||||
}
|
}
|
||||||
.border-bottom {
|
.border-bottom {
|
||||||
@apply border-b border-gray-100;
|
@apply border-b border-[#f0f0f0];
|
||||||
}
|
}
|
||||||
.border-top {
|
.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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Button, Card, Form, Divider, message } from "antd";
|
import { Button, Form, message } from "antd";
|
||||||
import { ArrowLeft, Play, BarChart3, Shuffle, PieChart } from "lucide-react";
|
import { ArrowLeft, ChevronRight } from "lucide-react";
|
||||||
import { createRatioTaskUsingPost } from "@/pages/RatioTask/ratio.api.ts";
|
import { createRatioTaskUsingPost } from "@/pages/RatioTask/ratio.api.ts";
|
||||||
import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
|
import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import SelectDataset from "@/pages/RatioTask/Create/components/SelectDataset.tsx";
|
import SelectDataset from "@/pages/RatioTask/Create/components/SelectDataset.tsx";
|
||||||
import BasicInformation from "@/pages/RatioTask/Create/components/BasicInformation.tsx";
|
import BasicInformation from "@/pages/RatioTask/Create/components/BasicInformation.tsx";
|
||||||
import RatioConfig from "@/pages/RatioTask/Create/components/RatioConfig.tsx";
|
import RatioConfig from "@/pages/RatioTask/Create/components/RatioConfig.tsx";
|
||||||
|
import RatioTransfer from "./components/RatioTransfer";
|
||||||
|
|
||||||
export default function CreateRatioTask() {
|
export default function CreateRatioTask() {
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
// 配比任务相关状态
|
// 配比任务相关状态
|
||||||
@@ -25,8 +25,9 @@ export default function CreateRatioTask() {
|
|||||||
|
|
||||||
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
const [distributions, setDistributions] = useState<Record<string, Record<string, number>>>({});
|
const [distributions, setDistributions] = useState<
|
||||||
|
Record<string, Record<string, number>>
|
||||||
|
>({});
|
||||||
|
|
||||||
const handleCreateRatioTask = async () => {
|
const handleCreateRatioTask = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -36,7 +37,8 @@ export default function CreateRatioTask() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Build request payload
|
// 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 totals = String(values.totalTargetCount);
|
||||||
const config = ratioTaskForm.ratioConfigs.map((c) => {
|
const config = ratioTaskForm.ratioConfigs.map((c) => {
|
||||||
if (ratio_method === "DATASET") {
|
if (ratio_method === "DATASET") {
|
||||||
@@ -69,11 +71,19 @@ export default function CreateRatioTask() {
|
|||||||
message.success("配比任务创建成功");
|
message.success("配比任务创建成功");
|
||||||
navigate("/data/synthesis/ratio-task");
|
navigate("/data/synthesis/ratio-task");
|
||||||
} catch {
|
} catch {
|
||||||
// 校验失败
|
message.error("配比任务创建失败,请重试");
|
||||||
} finally {
|
} finally {
|
||||||
setCreating(false);
|
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
|
// 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}`;
|
const sourceKey = `${datasetId}_${label}`;
|
||||||
setRatioTaskForm((prev) => {
|
setRatioTaskForm((prev) => {
|
||||||
const existingIndex = prev.ratioConfigs.findIndex((c) => c.source === sourceKey);
|
const existingIndex = prev.ratioConfigs.findIndex(
|
||||||
|
(c) => c.source === sourceKey
|
||||||
|
);
|
||||||
const totalOtherQuantity = prev.ratioConfigs
|
const totalOtherQuantity = prev.ratioConfigs
|
||||||
.filter((c) => c.source !== sourceKey)
|
.filter((c) => c.source !== sourceKey)
|
||||||
.reduce((sum, c) => sum + c.quantity, 0);
|
.reduce((sum, c) => sum + c.quantity, 0);
|
||||||
@@ -176,9 +192,9 @@ export default function CreateRatioTask() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="h-full flex flex-col gap-4">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
@@ -189,19 +205,37 @@ export default function CreateRatioTask() {
|
|||||||
<h1 className="text-xl font-bold bg-clip-text">创建配比任务</h1>
|
<h1 className="text-xl font-bold bg-clip-text">创建配比任务</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Card className="overflow-y-auto p-2">
|
<div className="h-full flex-overflow-auto border-card">
|
||||||
|
<div className="h-full overflow-auto p-6">
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
initialValues={ratioTaskForm}
|
initialValues={ratioTaskForm}
|
||||||
onValuesChange={handleValuesChange}
|
onValuesChange={handleValuesChange}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
|
className="h-full"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-12 gap-6">
|
<BasicInformation
|
||||||
{/* 左侧:数据集选择 */}
|
totalTargetCount={ratioTaskForm.totalTargetCount}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <RatioTransfer
|
||||||
|
ratioTaskForm={ratioTaskForm}
|
||||||
|
distributions={distributions}
|
||||||
|
updateRatioConfig={updateRatioConfig}
|
||||||
|
updateLabelRatioConfig={updateLabelRatioConfig}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
<div className="flex h-full">
|
||||||
<SelectDataset
|
<SelectDataset
|
||||||
selectedDatasets={ratioTaskForm.selectedDatasets}
|
selectedDatasets={ratioTaskForm.selectedDatasets}
|
||||||
ratioType={ratioTaskForm.ratioType}
|
ratioType={ratioTaskForm.ratioType}
|
||||||
onRatioTypeChange={(value) => setRatioTaskForm({ ...ratioTaskForm, ratioType: value, ratioConfigs: [] })}
|
onRatioTypeChange={(value) =>
|
||||||
|
setRatioTaskForm({
|
||||||
|
...ratioTaskForm,
|
||||||
|
ratioType: value,
|
||||||
|
ratioConfigs: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
onSelectedDatasetsChange={(next) => {
|
onSelectedDatasetsChange={(next) => {
|
||||||
setRatioTaskForm((prev) => ({
|
setRatioTaskForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -217,78 +251,25 @@ export default function CreateRatioTask() {
|
|||||||
onDistributionsChange={(next) => setDistributions(next)}
|
onDistributionsChange={(next) => setDistributions(next)}
|
||||||
onDatasetsChange={(list) => setDatasets(list)}
|
onDatasetsChange={(list) => setDatasets(list)}
|
||||||
/>
|
/>
|
||||||
{/* 右侧:配比配置 */}
|
<ChevronRight className="self-center" />
|
||||||
<div className="col-span-7">
|
|
||||||
<h2 className="font-medium text-gray-900 text-lg mb-2 flex items-center gap-2">
|
|
||||||
<PieChart className="w-5 h-5" />
|
|
||||||
配比配置
|
|
||||||
</h2>
|
|
||||||
<Card>
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div>
|
|
||||||
<span className="flex items-center gap-2 font-semibold">
|
|
||||||
<BarChart3 className="w-5 h-5" />
|
|
||||||
配比设置
|
|
||||||
</span>
|
|
||||||
<div className="text-gray-500 text-xs">
|
|
||||||
设置每个数据集的配比数量
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
icon={<Shuffle />}
|
|
||||||
size="small"
|
|
||||||
onClick={generateAutoRatio}
|
|
||||||
disabled={ratioTaskForm.selectedDatasets.length === 0}
|
|
||||||
>
|
|
||||||
平均分配
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<BasicInformation totalTargetCount={ratioTaskForm.totalTargetCount} />
|
|
||||||
<RatioConfig
|
<RatioConfig
|
||||||
ratioType={ratioTaskForm.ratioType}
|
ratioType={ratioTaskForm.ratioType}
|
||||||
selectedDatasets={ratioTaskForm.selectedDatasets}
|
selectedDatasets={ratioTaskForm.selectedDatasets}
|
||||||
datasets={datasets}
|
datasets={datasets}
|
||||||
ratioConfigs={ratioTaskForm.ratioConfigs as any}
|
|
||||||
totalTargetCount={ratioTaskForm.totalTargetCount}
|
totalTargetCount={ratioTaskForm.totalTargetCount}
|
||||||
distributions={distributions}
|
distributions={distributions}
|
||||||
onUpdateDatasetQuantity={(datasetId, quantity) => updateRatioConfig(datasetId, quantity)}
|
onChange={(configs) =>
|
||||||
onUpdateLabelQuantity={(datasetId, label, quantity) => updateLabelRatioConfig(datasetId, label, quantity)}
|
setRatioTaskForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
ratioConfigs: configs,
|
||||||
|
}))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{/* 配比预览 */}
|
|
||||||
{ratioTaskForm.ratioConfigs.length > 0 && (
|
|
||||||
<div className="mb-4">
|
|
||||||
<span className="text-sm font-medium">配比预览</span>
|
|
||||||
<div className="p-3 bg-gray-50 rounded-lg">
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<span className="text-gray-500">总配比数量:</span>
|
|
||||||
<span className="ml-2 font-medium">
|
|
||||||
{ratioTaskForm.ratioConfigs
|
|
||||||
.reduce((sum, config) => sum + config.quantity, 0)
|
|
||||||
.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</Form>
|
||||||
<span className="text-gray-500">目标数量:</span>
|
|
||||||
<span className="ml-2 font-medium">
|
|
||||||
{ratioTaskForm.totalTargetCount.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex justify-end gap-2 p-6">
|
||||||
<span className="text-gray-500">配比项目:</span>
|
<Button onClick={() => navigate("/data/synthesis/ratio-task")}>
|
||||||
<span className="ml-2 font-medium">
|
|
||||||
{ratioTaskForm.ratioConfigs.length}个
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Divider />
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
onClick={() => navigate("/data/synthesis/ratio-task")}
|
|
||||||
>
|
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -296,19 +277,13 @@ export default function CreateRatioTask() {
|
|||||||
onClick={handleCreateRatioTask}
|
onClick={handleCreateRatioTask}
|
||||||
loading={creating}
|
loading={creating}
|
||||||
disabled={
|
disabled={
|
||||||
!ratioTaskForm.name ||
|
!ratioTaskForm.name || ratioTaskForm.ratioConfigs.length === 0
|
||||||
ratioTaskForm.ratioConfigs.length === 0
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Play className="w-4 h-4 mr-2" />
|
创建
|
||||||
创建任务
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ interface BasicInformationProps {
|
|||||||
totalTargetCount: number;
|
totalTargetCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BasicInformation: React.FC<BasicInformationProps> = ({ totalTargetCount }) => {
|
const BasicInformation: React.FC<BasicInformationProps> = ({
|
||||||
|
totalTargetCount,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="任务名称"
|
label="任务名称"
|
||||||
name="name"
|
name="name"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { Badge, Card, Input, Progress } from "antd";
|
import { Badge, Card, Input, Progress, Button, Divider } from "antd";
|
||||||
import { BarChart3 } from "lucide-react";
|
import { BarChart3 } from "lucide-react";
|
||||||
import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
|
import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
|
||||||
|
|
||||||
@@ -16,32 +16,150 @@ interface RatioConfigProps {
|
|||||||
ratioType: "dataset" | "label";
|
ratioType: "dataset" | "label";
|
||||||
selectedDatasets: string[];
|
selectedDatasets: string[];
|
||||||
datasets: Dataset[];
|
datasets: Dataset[];
|
||||||
ratioConfigs: RatioConfigItem[];
|
|
||||||
totalTargetCount: number;
|
totalTargetCount: number;
|
||||||
distributions: Record<string, Record<string, number>>;
|
distributions: Record<string, Record<string, number>>;
|
||||||
onUpdateDatasetQuantity: (datasetId: string, quantity: number) => void;
|
onChange?: (configs: RatioConfigItem[]) => void;
|
||||||
onUpdateLabelQuantity: (datasetId: string, label: string, quantity: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RatioConfig: React.FC<RatioConfigProps> = ({
|
const RatioConfig: React.FC<RatioConfigProps> = ({
|
||||||
ratioType,
|
ratioType,
|
||||||
selectedDatasets,
|
selectedDatasets,
|
||||||
datasets,
|
datasets,
|
||||||
ratioConfigs,
|
|
||||||
totalTargetCount,
|
totalTargetCount,
|
||||||
distributions,
|
distributions,
|
||||||
onUpdateDatasetQuantity,
|
onChange,
|
||||||
onUpdateLabelQuantity,
|
|
||||||
}) => {
|
}) => {
|
||||||
const totalConfigured = ratioConfigs.reduce((sum, c) => sum + (c.quantity || 0), 0);
|
const [ratioConfigs, setRatioConfigs] = useState<RatioConfigItem[]>([]);
|
||||||
|
|
||||||
|
// 配比项总数
|
||||||
|
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 (
|
return (
|
||||||
<div className="mb-4">
|
<div className="border-card flex-1 flex flex-col min-w-[320px]">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between p-4 border-bottom">
|
||||||
<span className="text-sm font-medium">配比设置</span>
|
<span className="text-sm font-bold">
|
||||||
|
配比配置
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
已配置: {totalConfigured} / {totalTargetCount}
|
(已配置:{totalConfigured}/{totalTargetCount}条)
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
onClick={generateAutoRatio}
|
||||||
|
disabled={selectedDatasets.length === 0}
|
||||||
|
>
|
||||||
|
平均分配
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{selectedDatasets.length === 0 ? (
|
{selectedDatasets.length === 0 ? (
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500">
|
||||||
@@ -49,7 +167,37 @@ const RatioConfig: React.FC<RatioConfigProps> = ({
|
|||||||
<p className="text-sm">请先选择数据集</p>
|
<p className="text-sm">请先选择数据集</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ maxHeight: 500, overflowY: "auto" }}>
|
<div className="flex-overflow-auto gap-4 p-4">
|
||||||
|
{/* 配比预览 */}
|
||||||
|
{ratioConfigs.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">总配比数量:</span>
|
||||||
|
<span className="ml-2 font-medium">
|
||||||
|
{ratioConfigs
|
||||||
|
.reduce((sum, config) => sum + config.quantity, 0)
|
||||||
|
.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">目标数量:</span>
|
||||||
|
<span className="ml-2 font-medium">
|
||||||
|
{totalTargetCount.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">配比项目:</span>
|
||||||
|
<span className="ml-2 font-medium">
|
||||||
|
{ratioConfigs.length}个
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
{selectedDatasets.map((datasetId) => {
|
{selectedDatasets.map((datasetId) => {
|
||||||
const dataset = datasets.find((d) => String(d.id) === datasetId);
|
const dataset = datasets.find((d) => String(d.id) === datasetId);
|
||||||
const config = ratioConfigs.find((c) => c.source === datasetId);
|
const config = ratioConfigs.find((c) => c.source === datasetId);
|
||||||
@@ -59,10 +207,14 @@ const RatioConfig: React.FC<RatioConfigProps> = ({
|
|||||||
<Card key={datasetId} size="small" className="mb-2">
|
<Card key={datasetId} size="small" className="mb-2">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium text-sm">{dataset.name}</span>
|
<span className="font-medium text-sm">
|
||||||
|
{dataset.name}
|
||||||
|
</span>
|
||||||
<Badge color="gray">{dataset.fileCount}条</Badge>
|
<Badge color="gray">{dataset.fileCount}条</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500">{config?.percentage || 0}%</div>
|
<div className="text-xs text-gray-500">
|
||||||
|
{config?.percentage || 0}%
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ratioType === "dataset" ? (
|
{ratioType === "dataset" ? (
|
||||||
<div>
|
<div>
|
||||||
@@ -71,47 +223,82 @@ const RatioConfig: React.FC<RatioConfigProps> = ({
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={currentQuantity}
|
value={currentQuantity}
|
||||||
onChange={(e) => onUpdateDatasetQuantity(datasetId, Number(e.target.value))}
|
onChange={(e) =>
|
||||||
|
updateDatasetQuantity(
|
||||||
|
datasetId,
|
||||||
|
Number(e.target.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
style={{ width: 80 }}
|
style={{ width: 80 }}
|
||||||
min={0}
|
min={0}
|
||||||
max={Math.min(dataset.fileCount || 0, totalTargetCount)}
|
max={Math.min(
|
||||||
|
dataset.fileCount || 0,
|
||||||
|
totalTargetCount
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-500">条</span>
|
<span className="text-xs text-gray-500">条</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
percent={Math.round((currentQuantity / totalTargetCount) * 100)}
|
percent={Math.round(
|
||||||
|
(currentQuantity / totalTargetCount) * 100
|
||||||
|
)}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{!distributions[String(dataset.id)] ? (
|
{!distributions[String(dataset.id)] ? (
|
||||||
<div className="text-xs text-gray-400">加载标签分布...</div>
|
<div className="text-xs text-gray-400">
|
||||||
) : Object.entries(distributions[String(dataset.id)]).length === 0 ? (
|
加载标签分布...
|
||||||
<div className="text-xs text-gray-400">该数据集暂无标签</div>
|
</div>
|
||||||
|
) : Object.entries(distributions[String(dataset.id)])
|
||||||
|
.length === 0 ? (
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
该数据集暂无标签
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{Object.entries(distributions[String(dataset.id)]).map(([label, count]) => {
|
{Object.entries(
|
||||||
|
distributions[String(dataset.id)]
|
||||||
|
).map(([label, count]) => {
|
||||||
const sourceKey = `${datasetId}_${label}`;
|
const sourceKey = `${datasetId}_${label}`;
|
||||||
const labelConfig = ratioConfigs.find((c) => c.source === sourceKey);
|
const labelConfig = ratioConfigs.find(
|
||||||
|
(c) => c.source === sourceKey
|
||||||
|
);
|
||||||
const labelQuantity = labelConfig?.quantity || 0;
|
const labelQuantity = labelConfig?.quantity || 0;
|
||||||
return (
|
return (
|
||||||
<div key={label} className="flex items-center justify-between gap-2">
|
<div
|
||||||
|
key={label}
|
||||||
|
className="flex items-center justify-between gap-2"
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge color="gray">{label}</Badge>
|
<Badge color="gray">{label}</Badge>
|
||||||
<span className="text-xs text-gray-500">{count}条</span>
|
<span className="text-xs text-gray-500">
|
||||||
|
{count}条
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs">数量:</span>
|
<span className="text-xs">数量:</span>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={labelQuantity}
|
value={labelQuantity}
|
||||||
onChange={(e) => onUpdateLabelQuantity(datasetId, label, Number(e.target.value))}
|
onChange={(e) =>
|
||||||
|
updateLabelQuantity(
|
||||||
|
datasetId,
|
||||||
|
label,
|
||||||
|
Number(e.target.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
style={{ width: 80 }}
|
style={{ width: 80 }}
|
||||||
min={0}
|
min={0}
|
||||||
max={Math.min(Number(count) || 0, totalTargetCount)}
|
max={Math.min(
|
||||||
|
Number(count) || 0,
|
||||||
|
totalTargetCount
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-gray-500">条</span>
|
<span className="text-xs text-gray-500">
|
||||||
|
条
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -124,6 +311,7 @@ const RatioConfig: React.FC<RatioConfigProps> = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
169
frontend/src/pages/RatioTask/Create/components/RatioTransfer.tsx
Normal file
169
frontend/src/pages/RatioTask/Create/components/RatioTransfer.tsx
Normal file
@@ -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<string, Record<string, number>>;
|
||||||
|
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 (
|
||||||
|
<div className="flex">
|
||||||
|
<div className="border-card flex-1 mr-4">
|
||||||
|
<h3 className="p-2 border-bottom">{`${selectedDatasets.length} / ${datasets.length} 项`}</h3>
|
||||||
|
<SearchControls
|
||||||
|
searchTerm={searchParams.keyword}
|
||||||
|
onSearchChange={(keyword) =>
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<Table
|
||||||
|
rowSelection={{
|
||||||
|
onChange: (_, selectedRows) => {
|
||||||
|
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];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="border-card flex-1">
|
||||||
|
<RatioConfig
|
||||||
|
datasets={selectedDatasets}
|
||||||
|
ratioTaskForm={ratioTaskForm}
|
||||||
|
distributions={distributions}
|
||||||
|
onUpdateDatasetQuantity={updateRatioConfig}
|
||||||
|
onUpdateLabelQuantity={updateLabelRatioConfig}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,15 +1,21 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Badge, Button, Card, Checkbox, Input, Pagination, Select } from "antd";
|
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 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 {
|
interface SelectDatasetProps {
|
||||||
selectedDatasets: string[];
|
selectedDatasets: string[];
|
||||||
ratioType: "dataset" | "label";
|
ratioType: "dataset" | "label";
|
||||||
onRatioTypeChange: (val: "dataset" | "label") => void;
|
onRatioTypeChange: (val: "dataset" | "label") => void;
|
||||||
onSelectedDatasetsChange: (next: string[]) => void;
|
onSelectedDatasetsChange: (next: string[]) => void;
|
||||||
onDistributionsChange?: (next: Record<string, Record<string, number>>) => void;
|
onDistributionsChange?: (
|
||||||
|
next: Record<string, Record<string, number>>
|
||||||
|
) => void;
|
||||||
onDatasetsChange?: (list: Dataset[]) => void;
|
onDatasetsChange?: (list: Dataset[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +31,9 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [pagination, setPagination] = useState({ page: 1, size: 10, total: 0 });
|
const [pagination, setPagination] = useState({ page: 1, size: 10, total: 0 });
|
||||||
const [distributions, setDistributions] = useState<Record<string, Record<string, number>>>({});
|
const [distributions, setDistributions] = useState<
|
||||||
|
Record<string, Record<string, number>>
|
||||||
|
>({});
|
||||||
|
|
||||||
// Fetch dataset list
|
// Fetch dataset list
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -40,7 +48,10 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
|||||||
const list = data?.content || data?.data || [];
|
const list = data?.content || data?.data || [];
|
||||||
setDatasets(list);
|
setDatasets(list);
|
||||||
onDatasetsChange?.(list);
|
onDatasetsChange?.(list);
|
||||||
setPagination((prev) => ({ ...prev, total: data?.totalElements ?? data?.total ?? 0 }));
|
setPagination((prev) => ({
|
||||||
|
...prev,
|
||||||
|
total: data?.totalElements ?? data?.total ?? 0,
|
||||||
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -52,7 +63,9 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDistributions = async () => {
|
const fetchDistributions = async () => {
|
||||||
if (ratioType !== "label" || !datasets?.length) return;
|
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;
|
if (!idsToFetch.length) return;
|
||||||
try {
|
try {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
@@ -66,7 +79,9 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const next: Record<string, Record<string, number>> = { ...distributions };
|
const next: Record<string, Record<string, number>> = {
|
||||||
|
...distributions,
|
||||||
|
};
|
||||||
for (const { id, stats } of results) {
|
for (const { id, stats } of results) {
|
||||||
let dist: Record<string, number> | undefined = undefined;
|
let dist: Record<string, number> | undefined = undefined;
|
||||||
if (stats) {
|
if (stats) {
|
||||||
@@ -77,13 +92,16 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
|||||||
(stats as any).labels,
|
(stats as any).labels,
|
||||||
(stats as any).distribution,
|
(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)) {
|
if (Array.isArray(picked)) {
|
||||||
const obj: Record<string, number> = {};
|
const obj: Record<string, number> = {};
|
||||||
picked.forEach((it: any) => {
|
picked.forEach((it: any) => {
|
||||||
const key = it?.label ?? it?.name ?? it?.tag ?? it?.key;
|
const key = it?.label ?? it?.name ?? it?.tag ?? it?.key;
|
||||||
const val = it?.count ?? it?.value ?? it?.num ?? it?.total;
|
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;
|
dist = obj;
|
||||||
} else if (picked && typeof picked === "object") {
|
} else if (picked && typeof picked === "object") {
|
||||||
@@ -107,7 +125,8 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
|||||||
picked.forEach((it: any) => {
|
picked.forEach((it: any) => {
|
||||||
const key = it?.label ?? it?.name ?? it?.tag ?? it?.key;
|
const key = it?.label ?? it?.name ?? it?.tag ?? it?.key;
|
||||||
const val = it?.count ?? it?.value ?? it?.num ?? it?.total;
|
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;
|
dist = obj;
|
||||||
} else if (picked && typeof picked === "object") {
|
} else if (picked && typeof picked === "object") {
|
||||||
@@ -135,7 +154,9 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
|||||||
const next = Array.from(new Set([...selectedDatasets, datasetId]));
|
const next = Array.from(new Set([...selectedDatasets, datasetId]));
|
||||||
onSelectedDatasetsChange(next);
|
onSelectedDatasetsChange(next);
|
||||||
} else {
|
} else {
|
||||||
onSelectedDatasetsChange(selectedDatasets.filter((id) => id !== datasetId));
|
onSelectedDatasetsChange(
|
||||||
|
selectedDatasets.filter((id) => id !== datasetId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,16 +165,25 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="col-span-5">
|
<div className="border-card flex-1 flex flex-col min-w-[320px]">
|
||||||
<h2 className="font-medium text-gray-900 text-lg mb-2 flex items-center gap-2">
|
<div className="flex items-center justify-between p-4 border-bottom">
|
||||||
<Database className="w-5 h-5" />
|
<div className="flex items-center gap-4">
|
||||||
数据集选择
|
<span className="text-sm font-medium">
|
||||||
</h2>
|
选择数据集
|
||||||
<Card>
|
<span className="text-xs text-gray-500">
|
||||||
<div className="flex items-center gap-4 mb-4">
|
(已选择: {selectedDatasets.length}/{pagination.total})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button type="link" size="small" onClick={onClearSelection}>
|
||||||
|
清空选择
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-overflow-auto gap-4 p-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-sm">配比方式:</span>
|
<span className="text-sm">配比方式:</span>
|
||||||
<Select
|
<Select
|
||||||
style={{ width: 120 }}
|
className="flex-1 min-w-[120px]"
|
||||||
value={ratioType}
|
value={ratioType}
|
||||||
onChange={(v) => onRatioTypeChange(v)}
|
onChange={(v) => onRatioTypeChange(v)}
|
||||||
options={[
|
options={[
|
||||||
@@ -171,9 +201,11 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
|||||||
setPagination((p) => ({ ...p, page: 1 }));
|
setPagination((p) => ({ ...p, page: 1 }));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div style={{ maxHeight: 500, overflowY: "auto" }}>
|
<div className="flex-1 overflow-auto">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="text-center text-gray-500 py-8">正在加载数据集...</div>
|
<div className="text-center text-gray-500 py-8">
|
||||||
|
正在加载数据集...
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loading &&
|
{!loading &&
|
||||||
datasets.map((dataset) => {
|
datasets.map((dataset) => {
|
||||||
@@ -183,7 +215,9 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
|||||||
<Card
|
<Card
|
||||||
key={dataset.id}
|
key={dataset.id}
|
||||||
size="small"
|
size="small"
|
||||||
className={`mb-2 cursor-pointer ${checked ? "border-blue-500" : "hover:border-blue-200"}`}
|
className={`cursor-pointer ${
|
||||||
|
checked ? "border-blue-500" : "hover:border-blue-200"
|
||||||
|
}`}
|
||||||
onClick={() => onToggleDataset(idStr, !checked)}
|
onClick={() => onToggleDataset(idStr, !checked)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
@@ -193,10 +227,14 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
|||||||
/>
|
/>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium text-sm truncate">{dataset.name}</span>
|
<span className="font-medium text-sm truncate">
|
||||||
|
{dataset.name}
|
||||||
|
</span>
|
||||||
<Badge color="blue">{dataset.datasetType}</Badge>
|
<Badge color="blue">{dataset.datasetType}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 mt-1">{dataset.description}</div>
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
{dataset.description}
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500">
|
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500">
|
||||||
<span>{dataset.fileCount}条</span>
|
<span>{dataset.fileCount}条</span>
|
||||||
<span>{dataset.size}</span>
|
<span>{dataset.size}</span>
|
||||||
@@ -209,14 +247,21 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
|||||||
{Object.entries(distributions[idStr])
|
{Object.entries(distributions[idStr])
|
||||||
.slice(0, 8)
|
.slice(0, 8)
|
||||||
.map(([tag, count]) => (
|
.map(([tag, count]) => (
|
||||||
<Badge key={tag} color="gray">{`${tag}: ${count}`}</Badge>
|
<Badge
|
||||||
|
key={tag}
|
||||||
|
color="gray"
|
||||||
|
>{`${tag}: ${count}`}</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-gray-400">未检测到标签分布</div>
|
<div className="text-xs text-gray-400">
|
||||||
|
未检测到标签分布
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-gray-400">加载标签分布...</div>
|
<div className="text-xs text-gray-400">
|
||||||
|
加载标签分布...
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -227,22 +272,20 @@ const SelectDataset: React.FC<SelectDatasetProps> = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mt-3 items-center">
|
<div className="flex justify-between mt-3 items-center">
|
||||||
<span className="text-sm text-gray-600">已选择 {selectedDatasets.length} 个数据集</span>
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button size="small" onClick={onClearSelection}>
|
|
||||||
清空选择
|
|
||||||
</Button>
|
|
||||||
<Pagination
|
<Pagination
|
||||||
size="small"
|
size="small"
|
||||||
current={pagination.page}
|
current={pagination.page}
|
||||||
pageSize={pagination.size}
|
pageSize={pagination.size}
|
||||||
total={pagination.total}
|
total={pagination.total}
|
||||||
showSizeChanger
|
showSizeChanger
|
||||||
onChange={(p, ps) => setPagination((prev) => ({ ...prev, page: p, size: ps }))}
|
onChange={(p, ps) =>
|
||||||
|
setPagination((prev) => ({ ...prev, page: p, size: ps }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,59 +1,67 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button, Card, Table, Tooltip, App } from "antd";
|
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 { 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 { useNavigate } from "react-router";
|
||||||
import CardView from "@/components/CardView.tsx";
|
import CardView from "@/components/CardView";
|
||||||
import { SearchControls } from "@/components/SearchControls.tsx";
|
import { SearchControls } from "@/components/SearchControls";
|
||||||
import { queryRatioTasksUsingGet, deleteRatioTasksUsingDelete } from "@/pages/RatioTask/ratio.api.ts";
|
import {
|
||||||
|
deleteRatioTasksUsingDelete,
|
||||||
|
queryRatioTasksUsingGet,
|
||||||
|
} from "../ratio.api";
|
||||||
import useFetchData from "@/hooks/useFetchData";
|
import useFetchData from "@/hooks/useFetchData";
|
||||||
|
import { mapRatioTask, ratioTaskStatusMap } from "../ratio.const";
|
||||||
|
|
||||||
export default function RatioTasksPage() {
|
export default function RatioTasksPage() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const [viewMode, setViewMode] = useState<"card" | "list">("card");
|
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [viewMode, setViewMode] = useState<"card" | "list">("list");
|
||||||
|
|
||||||
const { loading, tableData, pagination, searchParams, setSearchParams, handleFiltersChange, fetchData } =
|
const {
|
||||||
useFetchData<RatioTaskItem>(queryRatioTasksUsingGet, (d) => d as RatioTaskItem, 30000, true, [], 0);
|
loading,
|
||||||
|
tableData,
|
||||||
|
pagination,
|
||||||
|
searchParams,
|
||||||
|
setSearchParams,
|
||||||
|
handleFiltersChange,
|
||||||
|
fetchData,
|
||||||
|
} = useFetchData<RatioTaskItem>(
|
||||||
|
queryRatioTasksUsingGet,
|
||||||
|
mapRatioTask,
|
||||||
|
30000,
|
||||||
|
true,
|
||||||
|
[],
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDeleteTask = async (task: RatioTaskItem) => {
|
||||||
await deleteRatioTasksUsingDelete([id]);
|
try {
|
||||||
message.success("删除成功");
|
// 调用删除接口
|
||||||
await fetchData();
|
await deleteRatioTasksUsingDelete(task.id);
|
||||||
|
message.success("任务删除成功");
|
||||||
|
// 重新加载数据
|
||||||
|
fetchData();
|
||||||
|
} catch (error) {
|
||||||
|
message.error("任务删除失败,请稍后重试");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusBadge = (status: string) => {
|
// 搜索、筛选和视图控制相关
|
||||||
const s = (status || "").toUpperCase();
|
const filters = [
|
||||||
const statusConfig = {
|
{
|
||||||
PENDING: {
|
key: "status",
|
||||||
label: "等待中",
|
label: "状态筛选",
|
||||||
color: "#f09e10ff",
|
options: [
|
||||||
icon: <Clock className="w-4 h-4 inline mr-1" />,
|
{ label: "全部状态", value: "all" },
|
||||||
|
{ label: "等待中", value: "PENDING" },
|
||||||
|
{ label: "运行中", value: "RUNNING" },
|
||||||
|
{ label: "已完成", value: "SUCCESS" },
|
||||||
|
{ label: "失败", value: "FAILED" },
|
||||||
|
{ label: "已暂停", value: "PAUSED" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
RUNNING: {
|
];
|
||||||
label: "运行中",
|
|
||||||
color: "#007bff",
|
|
||||||
icon: <Play className="w-4 h-4 inline mr-1" />,
|
|
||||||
},
|
|
||||||
SUCCESS: {
|
|
||||||
label: "已完成",
|
|
||||||
color: "#28a745",
|
|
||||||
icon: <CheckCircle className="w-4 h-4 inline mr-1" />,
|
|
||||||
},
|
|
||||||
FAILED: {
|
|
||||||
label: "失败",
|
|
||||||
color: "#dc3545",
|
|
||||||
icon: <AlertCircle className="w-4 h-4 inline mr-1" />,
|
|
||||||
},
|
|
||||||
PAUSED: {
|
|
||||||
label: "已暂停",
|
|
||||||
color: "#6c757d",
|
|
||||||
icon: <Pause className="w-4 h-4 inline mr-1" />,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return statusConfig[s as keyof typeof statusConfig] || statusConfig.PENDING;
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@@ -61,14 +69,20 @@ export default function RatioTasksPage() {
|
|||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
key: "name",
|
key: "name",
|
||||||
render: (text: string, record: RatioTaskItem) => (
|
render: (text: string, record: RatioTaskItem) => (
|
||||||
<a onClick={() => navigate(`/data/synthesis/ratio-task/detail/${record.id}`)}>{text}</a>
|
<a
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/data/synthesis/ratio-task/detail/${record.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "状态",
|
title: "状态",
|
||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
key: "status",
|
key: "status",
|
||||||
render: (v: string) => getStatusBadge(v).label,
|
render: (status) => ratioTaskStatusMap[status]?.label,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "配比方式",
|
title: "配比方式",
|
||||||
@@ -85,7 +99,13 @@ export default function RatioTasksPage() {
|
|||||||
dataIndex: "target_dataset_name",
|
dataIndex: "target_dataset_name",
|
||||||
key: "target_dataset_name",
|
key: "target_dataset_name",
|
||||||
render: (text: string, task: RatioTaskItem) => (
|
render: (text: string, task: RatioTaskItem) => (
|
||||||
<a onClick={() => navigate(`/data/management/detail/${task.target_dataset_id}`)}>{text}</a>
|
<a
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/data/management/detail/${task.target_dataset_id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -112,127 +132,25 @@ export default function RatioTasksPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderTableView = () => (
|
|
||||||
<Card>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
dataSource={tableData}
|
|
||||||
rowKey="id"
|
|
||||||
loading={loading}
|
|
||||||
pagination={pagination}
|
|
||||||
scroll={{ x: "max-content" }}
|
|
||||||
locale={{
|
|
||||||
emptyText: (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<BarChart3 className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
|
||||||
暂无配比任务
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-500 mb-4">
|
|
||||||
{searchParams.keyword || (searchParams.filter?.status?.[0] && searchParams.filter?.status?.[0] !== "all")
|
|
||||||
? "没有找到匹配的任务"
|
|
||||||
: "开始创建您的第一个配比任务"}
|
|
||||||
</p>
|
|
||||||
{!searchParams.keyword && (!searchParams.filter?.status?.length || searchParams.filter?.status?.[0] === "all") && (
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
navigate("/data/synthesis/ratio-task/create")
|
|
||||||
}
|
|
||||||
type="primary"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
|
||||||
创建配比任务
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
const operations = [
|
const operations = [
|
||||||
{
|
{
|
||||||
key: "delete",
|
key: "delete",
|
||||||
label: "删除",
|
label: "删除",
|
||||||
danger: true,
|
danger: true,
|
||||||
confirm: {
|
confirm: {
|
||||||
title: "确认删除该数据集?",
|
title: "确认删除该任务?",
|
||||||
description: "删除后该数据集将无法恢复,请谨慎操作。",
|
description: "删除后该任务将无法恢复,请谨慎操作。",
|
||||||
okText: "删除",
|
okText: "删除",
|
||||||
cancelText: "取消",
|
cancelText: "取消",
|
||||||
okType: "danger",
|
okType: "danger",
|
||||||
},
|
},
|
||||||
icon: <DeleteOutlined />,
|
icon: <DeleteOutlined />,
|
||||||
onClick: (item) => handleDelete(String(item.id)),
|
onClick: handleDeleteTask,
|
||||||
}
|
|
||||||
];
|
|
||||||
const renderCardView = () => (
|
|
||||||
<CardView
|
|
||||||
loading={loading}
|
|
||||||
data={tableData.map((task) => ({
|
|
||||||
...task,
|
|
||||||
description: task.ratio_method === "DATASET" ? "按数据集配比" : "按标签配比",
|
|
||||||
icon: <BarChart3 className="w-6 h-6" />,
|
|
||||||
iconColor: task.ratio_method === "DATASET" ? "bg-blue-100" : "bg-green-100",
|
|
||||||
statistics: [
|
|
||||||
{
|
|
||||||
label: "目标数量",
|
|
||||||
value: (task.totals ?? 0).toLocaleString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "目标数据集",
|
|
||||||
value: task.target_dataset_name ? (
|
|
||||||
<a onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
navigate(`/data/management/detail/${task.target_dataset_id}`);
|
|
||||||
}}>
|
|
||||||
{task.target_dataset_name}
|
|
||||||
</a>
|
|
||||||
) : '无',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 处理 SearchControls 的筛选变化
|
|
||||||
const handleSearchControlsFiltersChange = (
|
|
||||||
filters: Record<string, string[]>
|
|
||||||
) => {
|
|
||||||
handleFiltersChange(filters);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理视图切换
|
|
||||||
const handleViewModeChange = (mode: "card" | "list") => {
|
|
||||||
setViewMode(mode === "card" ? "card" : "list");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="h-full flex flex-col gap-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-xl font-bold">配比任务</h2>
|
<h2 className="text-xl font-bold">配比任务</h2>
|
||||||
<Button
|
<Button
|
||||||
@@ -247,17 +165,42 @@ export default function RatioTasksPage() {
|
|||||||
{/* 搜索、筛选和视图控制 */}
|
{/* 搜索、筛选和视图控制 */}
|
||||||
<SearchControls
|
<SearchControls
|
||||||
searchTerm={searchParams.keyword}
|
searchTerm={searchParams.keyword}
|
||||||
onSearchChange={(keyword) => setSearchParams({ ...searchParams, keyword })}
|
onSearchChange={(keyword) =>
|
||||||
searchPlaceholder="搜索任务名称"
|
setSearchParams({ ...searchParams, keyword })
|
||||||
filters={searchFilters}
|
}
|
||||||
onFiltersChange={handleSearchControlsFiltersChange}
|
searchPlaceholder="搜索任务名称..."
|
||||||
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
|
filters={filters}
|
||||||
viewMode={viewMode === "card" ? "card" : "list"}
|
onFiltersChange={handleFiltersChange}
|
||||||
onViewModeChange={handleViewModeChange}
|
onClearFilters={() =>
|
||||||
showViewToggle={true}
|
setSearchParams({ ...searchParams, filter: {} })
|
||||||
|
}
|
||||||
|
viewMode={viewMode}
|
||||||
|
onViewModeChange={setViewMode}
|
||||||
|
showViewToggle
|
||||||
|
onReload={fetchData}
|
||||||
/>
|
/>
|
||||||
{/* 任务列表 */}
|
{/* 任务列表 */}
|
||||||
{viewMode === "list" ? renderTableView() : renderCardView()}
|
{viewMode === "list" ? (
|
||||||
|
<Card>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={tableData}
|
||||||
|
pagination={pagination}
|
||||||
|
rowKey="id"
|
||||||
|
scroll={{ x: "max-content", y: "calc(100vh - 30rem)" }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<CardView
|
||||||
|
loading={loading}
|
||||||
|
data={tableData}
|
||||||
|
operations={operations}
|
||||||
|
pagination={pagination}
|
||||||
|
onView={(task) => {
|
||||||
|
navigate(`/data/synthesis/ratio-task/detail/${task.id}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
46
frontend/src/pages/RatioTask/ratio.const.tsx
Normal file
46
frontend/src/pages/RatioTask/ratio.const.tsx
Normal file
@@ -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>): RatioTaskItem {
|
||||||
|
return {
|
||||||
|
...task,
|
||||||
|
status: ratioTaskStatusMap[task.status || RatioStatus.PENDING]?.value,
|
||||||
|
createdAt: formatDate(task.created_at),
|
||||||
|
updatedAt: formatDate(task.updated_at),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,71 +1,80 @@
|
|||||||
// Ratio module models aligned with scripts/db/data-ratio-init.sql
|
// Ratio module models aligned with scripts/db/data-ratio-init.sql
|
||||||
|
|
||||||
// enums
|
// enums
|
||||||
export type RatioMethod = "TAG" | "DATASET"
|
export type RatioMethod = "TAG" | "DATASET";
|
||||||
export type RatioStatus = "PENDING" | "RUNNING" | "COMPLETED" | "FAILED" | "PAUSED"
|
|
||||||
|
export enum RatioStatus {
|
||||||
|
PENDING = "PENDING",
|
||||||
|
RUNNING = "RUNNING",
|
||||||
|
COMPLETED = "COMPLETED",
|
||||||
|
FAILED = "FAILED",
|
||||||
|
PAUSED = "PAUSED",
|
||||||
|
}
|
||||||
|
|
||||||
|
// interfaces
|
||||||
|
|
||||||
// t_st_ratio_instances
|
// t_st_ratio_instances
|
||||||
export interface RatioInstance {
|
export interface RatioInstance {
|
||||||
id: string
|
id: string;
|
||||||
name: string
|
name: string;
|
||||||
description?: string
|
description?: string;
|
||||||
targetDatasetId?: string
|
targetDatasetId?: string;
|
||||||
ratioMethod?: RatioMethod
|
ratioMethod?: RatioMethod;
|
||||||
ratioParameters?: any
|
ratioParameters?: any;
|
||||||
mergeMethod?: string
|
mergeMethod?: string;
|
||||||
status?: RatioStatus | string
|
status?: RatioStatus | string;
|
||||||
totals?: number
|
totals?: number;
|
||||||
createdAt?: string
|
createdAt?: string;
|
||||||
updatedAt?: string
|
updatedAt?: string;
|
||||||
createdBy?: string
|
createdBy?: string;
|
||||||
updatedBy?: string
|
updatedBy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// t_st_ratio_relations
|
// t_st_ratio_relations
|
||||||
export interface RatioRelation {
|
export interface RatioRelation {
|
||||||
id: string
|
id: string;
|
||||||
ratioInstanceId: string
|
ratioInstanceId: string;
|
||||||
sourceDatasetId?: string
|
sourceDatasetId?: string;
|
||||||
ratioValue?: string
|
ratioValue?: string;
|
||||||
counts?: number
|
counts?: number;
|
||||||
filterConditions?: string
|
filterConditions?: string;
|
||||||
createdAt?: string
|
createdAt?: string;
|
||||||
updatedAt?: string
|
updatedAt?: string;
|
||||||
createdBy?: string
|
createdBy?: string;
|
||||||
updatedBy?: string
|
updatedBy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API DTOs
|
// API DTOs
|
||||||
export interface RatioConfigItem {
|
export interface RatioConfigItem {
|
||||||
datasetId: string
|
datasetId: string;
|
||||||
counts: string
|
counts: string;
|
||||||
filter_conditions: string
|
filter_conditions: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateRatioTaskRequest {
|
export interface CreateRatioTaskRequest {
|
||||||
name: string
|
name: string;
|
||||||
description?: string
|
description?: string;
|
||||||
totals: string
|
totals: string;
|
||||||
ratio_method: RatioMethod
|
ratio_method: RatioMethod;
|
||||||
config: RatioConfigItem[]
|
config: RatioConfigItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TargetDatasetInfo {
|
export interface TargetDatasetInfo {
|
||||||
id: string
|
id: string;
|
||||||
name: string
|
name: string;
|
||||||
datasetType: string
|
datasetType: string;
|
||||||
status: string
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateRatioTaskResponse {
|
export interface CreateRatioTaskResponse {
|
||||||
id: string
|
id: string;
|
||||||
name: string
|
name: string;
|
||||||
description?: string
|
description?: string;
|
||||||
totals: number
|
totals: number;
|
||||||
ratio_method: RatioMethod
|
ratio_method: RatioMethod;
|
||||||
status: string
|
status: string;
|
||||||
config: RatioConfigItem[]
|
config: RatioConfigItem[];
|
||||||
targetDataset: TargetDatasetInfo
|
targetDataset: TargetDatasetInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RatioTaskItem {
|
export interface RatioTaskItem {
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export default defineConfig({
|
|||||||
// },
|
// },
|
||||||
proxy: {
|
proxy: {
|
||||||
"^/api": {
|
"^/api": {
|
||||||
// target: "http://localhost:8002", // 本地后端服务地址
|
target: "http://localhost:8002", // 本地后端服务地址
|
||||||
target: "http://1.94.5.242:32530", // 远程后端服务地址
|
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
rewrite: (path) => path.replace(/^\/api/, "/api"),
|
rewrite: (path) => path.replace(/^\/api/, "/api"),
|
||||||
|
|||||||
Reference in New Issue
Block a user