Files
DataMate/frontend/src/pages/RatioTask/CreateRatioTask.tsx

572 lines
22 KiB
TypeScript

import { useState } from "react";
import {
Button,
Card,
Input,
Select,
Badge,
Progress,
Checkbox,
Switch,
Form,
Divider,
message,
} from "antd";
import {
ArrowLeft,
Play,
Search as SearchIcon,
Database,
BarChart3,
Shuffle,
PieChart,
} from "lucide-react";
import type { RatioConfig, RatioTask } from "@/pages/RatioTask/ratio";
import { mockRatioTasks } from "@/mock/ratio";
import type { Dataset } from "@/pages/DataManagement/dataset.model";
import { useNavigate } from "react-router";
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
const { TextArea } = Input;
const { Option } = Select;
export default function CreateRatioTask() {
return <DevelopmentInProgress showTime="2025.11.30" />;
const navigate = useNavigate();
const [form] = Form.useForm();
// 配比任务相关状态
const [ratioTaskForm, setRatioTaskForm] = useState({
name: "",
description: "",
ratioType: "dataset" as "dataset" | "label",
selectedDatasets: [] as string[],
ratioConfigs: [] as RatioConfig[],
totalTargetCount: 10000,
autoStart: true,
});
const [tasks, setTasks] = useState<RatioTask[]>(mockRatioTasks);
const [datasets] = useState<Dataset[]>([]);
const handleCreateRatioTask = async () => {
try {
const values = await form.validateFields();
if (!ratioTaskForm.ratioConfigs.length) {
message.error("请配置配比项");
return;
}
const newTask: RatioTask = {
id: Date.now(),
name: values.name,
status: ratioTaskForm.autoStart ? "pending" : "paused",
progress: 0,
sourceDatasets: ratioTaskForm.selectedDatasets,
targetCount: values.totalTargetCount,
generatedCount: 0,
createdAt: new Date().toISOString().split("T")[0],
ratioType: ratioTaskForm.ratioType,
estimatedTime: "预计 20 分钟",
ratioConfigs: ratioTaskForm.ratioConfigs,
};
setTasks([newTask, ...tasks]);
setRatioTaskForm({
name: "",
description: "",
ratioType: "dataset",
selectedDatasets: [],
ratioConfigs: [],
totalTargetCount: 10000,
autoStart: true,
});
form.resetFields();
message.success("配比任务创建成功");
navigate("/data/ratio-task");
} catch {
// 校验失败
}
};
const handleDatasetSelection = (datasetId: string, checked: boolean) => {
if (checked) {
setRatioTaskForm((prev) => ({
...prev,
selectedDatasets: [...prev.selectedDatasets, datasetId],
}));
} else {
setRatioTaskForm((prev) => ({
...prev,
selectedDatasets: prev.selectedDatasets.filter(
(id) => id !== datasetId
),
ratioConfigs: prev.ratioConfigs.filter(
(config) => config.source !== datasetId
),
}));
}
};
const updateRatioConfig = (source: string, quantity: number) => {
setRatioTaskForm((prev) => {
const existingIndex = prev.ratioConfigs.findIndex(
(config) => config.source === source
);
const totalOtherQuantity = prev.ratioConfigs
.filter((config) => config.source !== source)
.reduce((sum, config) => sum + config.quantity, 0);
const newConfig: RatioConfig = {
id: source,
name: source,
type: prev.ratioType,
quantity: Math.min(
quantity,
prev.totalTargetCount - totalOtherQuantity
),
percentage: Math.round((quantity / prev.totalTargetCount) * 100),
source,
};
if (existingIndex >= 0) {
const newConfigs = [...prev.ratioConfigs];
newConfigs[existingIndex] = newConfig;
return { ...prev, ratioConfigs: newConfigs };
} else {
return { ...prev, ratioConfigs: [...prev.ratioConfigs, newConfig] };
}
});
};
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: RatioConfig[] = 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 }));
};
const handleValuesChange = (_, allValues) => {
setRatioTaskForm({ ...ratioTaskForm, ...allValues });
};
return (
<div className="min-h-screen">
{/* Header */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center">
<Button
type="text"
onClick={() => navigate("/data/synthesis/ratio-task")}
>
<ArrowLeft className="w-4 h-4 mr-1" />
</Button>
<h1 className="text-xl font-bold bg-clip-text"></h1>
</div>
</div>
<Card className="overflow-y-auto p-2">
<Form
form={form}
initialValues={ratioTaskForm}
onValuesChange={handleValuesChange}
layout="vertical"
>
<div className="grid grid-cols-12 gap-6">
{/* 左侧:数据集选择 */}
<div className="col-span-5">
<h2 className="font-medium text-gray-900 text-lg mb-2 flex items-center gap-2">
<Database className="w-5 h-5" />
</h2>
<Card>
<div className="flex items-center gap-4 mb-4">
<div className="flex items-center gap-2">
<span className="text-sm">:</span>
<Form.Item name="ratioType" noStyle>
<Select
style={{ width: 120 }}
onChange={(value: "dataset" | "label") =>
setRatioTaskForm({
...ratioTaskForm,
ratioType: value,
ratioConfigs: [],
})
}
>
<Option value="dataset"></Option>
<Option value="label"></Option>
</Select>
</Form.Item>
</div>
<Input
prefix={<SearchIcon className="text-gray-400" />}
placeholder="搜索数据集"
style={{ width: 180 }}
// 可加搜索逻辑
/>
</div>
<div style={{ maxHeight: 500, overflowY: "auto" }}>
{datasets.map((dataset) => (
<Card
key={dataset.id}
size="small"
className={`mb-2 cursor-pointer ${
ratioTaskForm.selectedDatasets.includes(dataset.id)
? "border-blue-500"
: "hover:border-blue-200"
}`}
onClick={() =>
handleDatasetSelection(
dataset.id,
!ratioTaskForm.selectedDatasets.includes(dataset.id)
)
}
>
<div className="flex items-start gap-3">
<Checkbox
checked={ratioTaskForm.selectedDatasets.includes(
dataset.id
)}
onChange={(e) =>
handleDatasetSelection(dataset.id, e.target.checked)
}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm truncate">
{dataset.name}
</span>
<Badge color="blue">{dataset.type}</Badge>
</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">
<span>{dataset.records?.toLocaleString()}</span>
<span>{dataset.size}</span>
<span>{dataset.format}</span>
</div>
{ratioTaskForm.ratioType === "label" &&
dataset.labels && (
<div className="flex flex-wrap gap-1 mt-2">
{dataset.labels.map((label, index) => (
<Badge key={index} color="gray">
{label}
</Badge>
))}
</div>
)}
</div>
</div>
</Card>
))}
</div>
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg mt-4">
<span className="text-sm text-gray-600">
{ratioTaskForm.selectedDatasets.length}
</span>
<Button
size="small"
onClick={() =>
setRatioTaskForm({
...ratioTaskForm,
selectedDatasets: [],
ratioConfigs: [],
})
}
>
</Button>
</div>
</Card>
</div>
{/* 右侧:配比配置 */}
<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>
<div className="grid grid-cols-2 gap-4 mb-4">
<Form.Item
label="任务名称"
name="name"
rules={[{ required: true, message: "请输入配比任务名称" }]}
>
<Input
placeholder="输入配比任务名称"
value={ratioTaskForm.name}
/>
</Form.Item>
<Form.Item
label="目标总数量"
name="totalTargetCount"
rules={[{ required: true, message: "请输入目标总数量" }]}
>
<Input
type="number"
placeholder="目标总数量"
min={1}
value={ratioTaskForm.totalTargetCount}
/>
</Form.Item>
</div>
<Form.Item label="任务描述" name="description">
<TextArea
placeholder="描述配比任务的目的和要求(可选)"
rows={2}
value={ratioTaskForm.description}
/>
</Form.Item>
<div className="mb-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium"></span>
<span className="text-xs text-gray-500">
:{" "}
{ratioTaskForm.ratioConfigs.reduce(
(sum, config) => sum + config.quantity,
0
)}{" "}
/ {ratioTaskForm.totalTargetCount}
</span>
</div>
{ratioTaskForm.selectedDatasets.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<BarChart3 className="w-12 h-12 mx-auto mb-2 text-gray-300" />
<p className="text-sm"></p>
</div>
) : (
<div style={{ maxHeight: 500, overflowY: "auto" }}>
{ratioTaskForm.selectedDatasets.map((datasetId) => {
const dataset = datasets.find(
(d) => d.id === datasetId
);
const config = ratioTaskForm.ratioConfigs.find(
(c) => c.source === datasetId
);
const currentQuantity = config?.quantity || 0;
if (!dataset) return null;
return (
<Card key={datasetId} size="small" className="mb-2">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">
{dataset.name}
</span>
<Badge color="gray">
{dataset.records.toLocaleString()}
</Badge>
</div>
<div className="text-xs text-gray-500">
{config?.percentage || 0}%
</div>
</div>
{ratioTaskForm.ratioType === "dataset" ? (
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-xs">:</span>
<Input
type="number"
value={currentQuantity}
onChange={(e) =>
updateRatioConfig(
datasetId,
Number(e.target.value)
)
}
style={{ width: 80 }}
min={0}
max={ratioTaskForm.totalTargetCount}
/>
<span className="text-xs text-gray-500">
</span>
</div>
<Progress
percent={Math.round(
(currentQuantity /
ratioTaskForm.totalTargetCount) *
100
)}
size="small"
/>
</div>
) : (
<div>
{dataset.labels?.map((label, index) => {
const labelConfig =
ratioTaskForm.ratioConfigs.find(
(c) =>
c.source === `${datasetId}_${label}`
);
const labelQuantity =
labelConfig?.quantity || 0;
return (
<div
key={index}
className="flex items-center gap-2 mb-2"
>
<Badge color="gray">{label}</Badge>
<Input
type="number"
value={labelQuantity}
onChange={(e) =>
updateRatioConfig(
`${datasetId}_${label}`,
Number(e.target.value)
)
}
style={{ width: 70 }}
min={0}
/>
<span className="text-xs text-gray-500">
</span>
<Progress
percent={Math.round(
(labelQuantity /
ratioTaskForm.totalTargetCount) *
100
)}
size="small"
style={{ width: 80 }}
/>
<span className="text-xs text-gray-500 min-w-8">
{Math.round(
(labelQuantity /
ratioTaskForm.totalTargetCount) *
100
)}
%
</span>
</div>
);
})}
</div>
)}
</Card>
);
})}
</div>
)}
</div>
{/* 配比预览 */}
{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>
<span className="text-gray-500">:</span>
<span className="ml-2 font-medium">
{ratioTaskForm.totalTargetCount.toLocaleString()}
</span>
</div>
<div>
<span className="text-gray-500">:</span>
<span className="ml-2 font-medium">
{ratioTaskForm.ratioConfigs.length}
</span>
</div>
<div>
<span className="text-gray-500">:</span>
<span className="ml-2 font-medium"> 20 </span>
</div>
</div>
</div>
</div>
)}
<div className="flex items-center justify-between p-3 border rounded-lg mb-4">
<div>
<span className="text-sm font-medium"></span>
<div className="text-xs text-gray-500 mt-1">
</div>
</div>
<Form.Item name="autoStart" valuePropName="checked" noStyle>
<Switch
checked={ratioTaskForm.autoStart}
onChange={(checked) =>
setRatioTaskForm({
...ratioTaskForm,
autoStart: checked,
})
}
/>
</Form.Item>
</div>
<Divider />
<div className="flex justify-end gap-2">
<Button
onClick={() => navigate("/data/synthesis/ratio-task")}
>
</Button>
<Button
type="primary"
onClick={handleCreateRatioTask}
disabled={
!ratioTaskForm.name ||
ratioTaskForm.ratioConfigs.length === 0
}
>
<Play className="w-4 h-4 mr-2" />
</Button>
</div>
</Card>
</div>
</div>
</Form>
</Card>
</div>
);
}