feature:增加数据配比功能 (#52)

* refactor: 修改调整数据归集实现,删除无用代码,优化代码结构

* feature: 每天凌晨00:00扫描所有数据集,检查数据集是否超过了预设的保留天数,超出保留天数的数据集调用删除接口进行删除

* fix: 修改删除数据集文件的逻辑,上传到数据集中的文件会同时删除数据库中的记录和文件系统中的文件,归集过来的文件仅删除数据库中的记录

* fix: 增加参数校验和接口定义,删除不使用的接口

* fix: 数据集统计数据默认为0

* feature: 数据集状态增加流转,创建时为草稿状态,上传文件或者归集文件后修改为活动状态

* refactor: 修改分页查询归集任务的代码

* fix: 更新后重新执行;归集任务执行增加事务控制

* feature: 创建归集任务时能够同步创建数据集,更新归集任务时能更新到指定数据集

* fix: 创建归集任务不需要创建数据集时不应该报错

* fix: 修复删除文件时数据集的统计数据不变动

* feature: 查询数据集详情时能够获取到文件标签分布

* fix: tags为空时不进行分析

* fix: 状态修改为ACTIVE

* fix: 修改解析tag的方法

* feature: 实现创建、分页查询、删除配比任务

* feature: 实现创建、分页查询、删除配比任务的前端交互

* fix: 修复进度计算异常导致的页面报错
This commit is contained in:
hefanli
2025-11-03 10:17:39 +08:00
committed by GitHub
parent 07edf16044
commit 08bd4eca5c
32 changed files with 1894 additions and 1028 deletions

View File

@@ -49,11 +49,13 @@ export interface Dataset {
status: DatasetStatus;
size?: string;
itemCount?: number;
fileCount?: number;
createdBy: string;
createdAt: string;
updatedAt: string;
tags: string[];
targetLocation?: string;
distribution?: Record<string, number>;
}
export interface TagItem {

View File

@@ -0,0 +1,314 @@
import { useState } from "react";
import { Button, Card, Form, Divider, message } from "antd";
import { ArrowLeft, Play, BarChart3, Shuffle, PieChart } 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";
export default function CreateRatioTask() {
const navigate = useNavigate();
const [form] = Form.useForm();
// 配比任务相关状态
const [ratioTaskForm, setRatioTaskForm] = useState({
name: "",
description: "",
ratioType: "dataset" as "dataset" | "label",
selectedDatasets: [] as string[],
ratioConfigs: [] as any[],
totalTargetCount: 10000,
autoStart: true,
});
const [datasets, setDatasets] = useState<Dataset[]>([]);
const [creating, setCreating] = useState(false);
const [distributions, setDistributions] = useState<Record<string, Record<string, number>>>({});
const handleCreateRatioTask = async () => {
try {
const values = await form.validateFields();
if (!ratioTaskForm.ratioConfigs.length) {
message.error("请配置配比项");
return;
}
// Build request payload
const ratio_method = ratioTaskForm.ratioType === "dataset" ? "DATASET" : "TAG";
const totals = String(values.totalTargetCount);
const config = ratioTaskForm.ratioConfigs.map((c) => {
if (ratio_method === "DATASET") {
return {
datasetId: String(c.source),
counts: String(c.quantity ?? 0),
filter_conditions: "",
};
}
// TAG mode: source key like `${datasetId}_${label}`
const source = String(c.source || "");
const idx = source.indexOf("_");
const datasetId = idx > 0 ? source.slice(0, idx) : source;
const label = idx > 0 ? source.slice(idx + 1) : "";
return {
datasetId,
counts: String(c.quantity ?? 0),
filter_conditions: label ? JSON.stringify({ label }) : "",
};
});
setCreating(true);
await createRatioTaskUsingPost({
name: values.name,
description: values.description,
totals,
ratio_method,
config,
});
message.success("配比任务创建成功");
navigate("/data/synthesis/ratio-task");
} catch {
// 校验失败
} finally {
setCreating(false);
}
};
// dataset selection is handled inside SelectDataset via onSelectedDatasetsChange
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 = {
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 = 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 updateLabelRatioConfig = (datasetId: string, label: string, quantity: number) => {
const sourceKey = `${datasetId}_${label}`;
setRatioTaskForm((prev) => {
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);
const dist = distributions[datasetId] || {};
const labelMax = dist[label] ?? Infinity;
const cappedQuantity = Math.max(
0,
Math.min(quantity, prev.totalTargetCount - totalOtherQuantity, labelMax)
);
const newConfig = {
id: sourceKey,
name: label,
type: "label",
quantity: cappedQuantity,
percentage: Math.round((cappedQuantity / prev.totalTargetCount) * 100),
source: sourceKey,
};
if (existingIndex >= 0) {
const newConfigs = [...prev.ratioConfigs];
newConfigs[existingIndex] = newConfig;
return { ...prev, ratioConfigs: newConfigs };
} else {
return { ...prev, ratioConfigs: [...prev.ratioConfigs, newConfig] };
}
});
};
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">
{/* 左侧:数据集选择 */}
<SelectDataset
selectedDatasets={ratioTaskForm.selectedDatasets}
ratioType={ratioTaskForm.ratioType}
onRatioTypeChange={(value) => 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)}
/>
{/* 右侧:配比配置 */}
<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
ratioType={ratioTaskForm.ratioType}
selectedDatasets={ratioTaskForm.selectedDatasets}
datasets={datasets}
ratioConfigs={ratioTaskForm.ratioConfigs as any}
totalTargetCount={ratioTaskForm.totalTargetCount}
distributions={distributions}
onUpdateDatasetQuantity={(datasetId, quantity) => updateRatioConfig(datasetId, quantity)}
onUpdateLabelQuantity={(datasetId, label, quantity) => updateLabelRatioConfig(datasetId, label, quantity)}
/>
{/* 配比预览 */}
{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>
</div>
</div>
)}
<Divider />
<div className="flex justify-end gap-2">
<Button
onClick={() => navigate("/data/synthesis/ratio-task")}
>
</Button>
<Button
type="primary"
onClick={handleCreateRatioTask}
loading={creating}
disabled={
!ratioTaskForm.name ||
ratioTaskForm.ratioConfigs.length === 0
}
>
<Play className="w-4 h-4 mr-2" />
</Button>
</div>
</Card>
</div>
</div>
</Form>
</Card>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import React from "react";
import { Form, Input } from "antd";
const { TextArea } = Input;
interface BasicInformationProps {
totalTargetCount: number;
}
const BasicInformation: React.FC<BasicInformationProps> = ({ totalTargetCount }) => {
return (
<div className="grid grid-cols-2 gap-4 mb-4">
<Form.Item
label="任务名称"
name="name"
rules={[{ required: true, message: "请输入配比任务名称" }]}
>
<Input placeholder="输入配比任务名称" />
</Form.Item>
<Form.Item
label="目标总数量"
name="totalTargetCount"
rules={[{ required: true, message: "请输入目标总数量" }]}
>
<Input type="number" placeholder="目标总数量" min={1} />
</Form.Item>
<Form.Item label="任务描述" name="description" className="col-span-2">
<TextArea placeholder="描述配比任务的目的和要求(可选)" rows={2} />
</Form.Item>
</div>
);
};
export default BasicInformation;

View File

@@ -0,0 +1,132 @@
import React from "react";
import { Badge, Card, Input, Progress } from "antd";
import { BarChart3 } from "lucide-react";
import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
interface RatioConfigItem {
id: string;
name: string;
type: "dataset" | "label";
quantity: number;
percentage: number;
source: string;
}
interface RatioConfigProps {
ratioType: "dataset" | "label";
selectedDatasets: string[];
datasets: Dataset[];
ratioConfigs: RatioConfigItem[];
totalTargetCount: number;
distributions: Record<string, Record<string, number>>;
onUpdateDatasetQuantity: (datasetId: string, quantity: number) => void;
onUpdateLabelQuantity: (datasetId: string, label: string, quantity: number) => void;
}
const RatioConfig: React.FC<RatioConfigProps> = ({
ratioType,
selectedDatasets,
datasets,
ratioConfigs,
totalTargetCount,
distributions,
onUpdateDatasetQuantity,
onUpdateLabelQuantity,
}) => {
const totalConfigured = ratioConfigs.reduce((sum, c) => sum + (c.quantity || 0), 0);
return (
<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">
: {totalConfigured} / {totalTargetCount}
</span>
</div>
{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" }}>
{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 (
<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.fileCount}</Badge>
</div>
<div className="text-xs text-gray-500">{config?.percentage || 0}%</div>
</div>
{ratioType === "dataset" ? (
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-xs">:</span>
<Input
type="number"
value={currentQuantity}
onChange={(e) => onUpdateDatasetQuantity(datasetId, Number(e.target.value))}
style={{ width: 80 }}
min={0}
max={Math.min(dataset.fileCount || 0, totalTargetCount)}
/>
<span className="text-xs text-gray-500"></span>
</div>
<Progress
percent={Math.round((currentQuantity / totalTargetCount) * 100)}
size="small"
/>
</div>
) : (
<div>
{!distributions[String(dataset.id)] ? (
<div className="text-xs text-gray-400">...</div>
) : Object.entries(distributions[String(dataset.id)]).length === 0 ? (
<div className="text-xs text-gray-400"></div>
) : (
<div className="flex flex-col gap-2">
{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 (
<div key={label} className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Badge color="gray">{label}</Badge>
<span className="text-xs text-gray-500">{count}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs">:</span>
<Input
type="number"
value={labelQuantity}
onChange={(e) => onUpdateLabelQuantity(datasetId, label, Number(e.target.value))}
style={{ width: 80 }}
min={0}
max={Math.min(Number(count) || 0, totalTargetCount)}
/>
<span className="text-xs text-gray-500"></span>
</div>
</div>
);
})}
</div>
)}
</div>
)}
</Card>
);
})}
</div>
)}
</div>
);
};
export default RatioConfig;

View File

@@ -0,0 +1,250 @@
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 type { Dataset } from "@/pages/DataManagement/dataset.model.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<string, Record<string, number>>) => void;
onDatasetsChange?: (list: Dataset[]) => void;
}
const SelectDataset: React.FC<SelectDatasetProps> = ({
selectedDatasets,
ratioType,
onRatioTypeChange,
onSelectedDatasetsChange,
onDistributionsChange,
onDatasetsChange,
}) => {
const [datasets, setDatasets] = useState<Dataset[]>([]);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [pagination, setPagination] = useState({ page: 1, size: 10, total: 0 });
const [distributions, setDistributions] = useState<Record<string, Record<string, number>>>({});
// Fetch dataset list
useEffect(() => {
const fetchDatasets = async () => {
try {
setLoading(true);
const { data } = await queryDatasetsUsingGet({
page: pagination.page,
size: pagination.size,
keyword: searchQuery?.trim() || undefined,
});
const list = data?.content || data?.data || [];
setDatasets(list);
onDatasetsChange?.(list);
setPagination((prev) => ({ ...prev, total: data?.totalElements ?? data?.total ?? 0 }));
} finally {
setLoading(false);
}
};
fetchDatasets();
}, [pagination.page, pagination.size, searchQuery]);
// Fetch label distributions when in label mode
useEffect(() => {
const fetchDistributions = async () => {
if (ratioType !== "label" || !datasets?.length) return;
const idsToFetch = datasets.map((d) => String(d.id)).filter((id) => !distributions[id]);
if (!idsToFetch.length) return;
try {
const results = await Promise.all(
idsToFetch.map(async (id) => {
try {
const statRes = await queryDatasetStatisticsByIdUsingGet(id);
return { id, stats: statRes?.data };
} catch {
return { id, stats: null };
}
})
);
const next: Record<string, Record<string, number>> = { ...distributions };
for (const { id, stats } of results) {
let dist: Record<string, number> | undefined = undefined;
if (stats) {
const candidates: any[] = [
(stats as any).labelDistribution,
(stats as any).tagDistribution,
(stats as any).label_stats,
(stats as any).labels,
(stats as any).distribution,
];
let picked = candidates.find((c) => c && (typeof c === "object" || Array.isArray(c)));
if (Array.isArray(picked)) {
const obj: Record<string, number> = {};
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;
});
dist = obj;
} else if (picked && typeof picked === "object") {
dist = picked as Record<string, number>;
}
}
if (!dist) {
try {
const detRes = await queryDatasetByIdUsingGet(id);
const det = detRes?.data;
if (det) {
let picked =
(det as any).distribution ||
(det as any).labelDistribution ||
(det as any).tagDistribution ||
(det as any).label_stats ||
(det as any).labels ||
undefined;
if (Array.isArray(picked)) {
const obj: Record<string, number> = {};
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;
});
dist = obj;
} else if (picked && typeof picked === "object") {
dist = picked as Record<string, number>;
}
}
} catch {
dist = undefined;
}
}
next[String(id)] = dist || {};
}
setDistributions(next);
onDistributionsChange?.(next);
} catch {
// ignore
}
};
fetchDistributions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ratioType, datasets]);
const onToggleDataset = (datasetId: string, checked: boolean) => {
if (checked) {
const next = Array.from(new Set([...selectedDatasets, datasetId]));
onSelectedDatasetsChange(next);
} else {
onSelectedDatasetsChange(selectedDatasets.filter((id) => id !== datasetId));
}
};
const onClearSelection = () => {
onSelectedDatasetsChange([]);
};
return (
<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">
<span className="text-sm">:</span>
<Select
style={{ width: 120 }}
value={ratioType}
onChange={(v) => onRatioTypeChange(v)}
options={[
{ label: "按数据集", value: "dataset" },
{ label: "按标签", value: "label" },
]}
/>
</div>
<Input
prefix={<SearchIcon className="text-gray-400" />}
placeholder="搜索数据集"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setPagination((p) => ({ ...p, page: 1 }));
}}
/>
<div style={{ maxHeight: 500, overflowY: "auto" }}>
{loading && (
<div className="text-center text-gray-500 py-8">...</div>
)}
{!loading &&
datasets.map((dataset) => {
const idStr = String(dataset.id);
const checked = selectedDatasets.includes(idStr);
return (
<Card
key={dataset.id}
size="small"
className={`mb-2 cursor-pointer ${checked ? "border-blue-500" : "hover:border-blue-200"}`}
onClick={() => onToggleDataset(idStr, !checked)}
>
<div className="flex items-start gap-3">
<Checkbox
checked={checked}
onChange={(e) => onToggleDataset(idStr, 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.datasetType}</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.fileCount}</span>
<span>{dataset.size}</span>
</div>
{ratioType === "label" && (
<div className="mt-2">
{distributions[idStr] ? (
Object.entries(distributions[idStr]).length > 0 ? (
<div className="flex flex-wrap gap-2 text-xs">
{Object.entries(distributions[idStr])
.slice(0, 8)
.map(([tag, count]) => (
<Badge key={tag} color="gray">{`${tag}: ${count}`}</Badge>
))}
</div>
) : (
<div className="text-xs text-gray-400"></div>
)
) : (
<div className="text-xs text-gray-400">...</div>
)}
</div>
)}
</div>
</div>
</Card>
);
})}
</div>
<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">
<Button size="small" onClick={onClearSelection}>
</Button>
<Pagination
size="small"
current={pagination.page}
pageSize={pagination.size}
total={pagination.total}
showSizeChanger
onChange={(p, ps) => setPagination((prev) => ({ ...prev, page: p, size: ps }))}
/>
</div>
</div>
</Card>
</div>
);
};
export default SelectDataset;

View File

@@ -1,571 +0,0 @@
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>
);
}

View File

@@ -0,0 +1,246 @@
import { useState } from "react";
import { Button, Card, Table, Tooltip, App } from "antd";
import { Plus, Clock, Play, CheckCircle, AlertCircle, Pause, BarChart3 } from "lucide-react";
import { DeleteOutlined } from "@ant-design/icons";
import type { RatioTaskItem } from "@/pages/RatioTask/ratio.model.ts";
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 useFetchData from "@/hooks/useFetchData";
export default function RatioTasksPage() {
const navigate = useNavigate();
const [viewMode, setViewMode] = useState<"card" | "list">("card");
const { message } = App.useApp();
const { loading, tableData, pagination, searchParams, setSearchParams, handleFiltersChange, fetchData } =
useFetchData<RatioTaskItem>(queryRatioTasksUsingGet, (d) => d as RatioTaskItem, 30000, true, [], 0);
const handleDelete = async (id: string) => {
await deleteRatioTasksUsingDelete([id]);
message.success("删除成功");
await fetchData();
};
const getStatusBadge = (status: string) => {
const s = (status || "").toUpperCase();
const statusConfig = {
PENDING: {
label: "等待中",
color: "#f09e10ff",
icon: <Clock className="w-4 h-4 inline mr-1" />,
},
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 = [
{
title: "任务名称",
dataIndex: "name",
key: "name",
},
{
title: "状态",
dataIndex: "status",
key: "status",
render: (v: string) => getStatusBadge(v).label,
},
{
title: "配比方式",
dataIndex: "ratio_method",
key: "ratio_method",
},
{
title: "目标数量",
dataIndex: "totals",
key: "totals",
},
{
title: "目标数据集",
dataIndex: "target_dataset_name",
key: "target_dataset_name",
},
{
title: "创建时间",
dataIndex: "created_at",
key: "created_at",
},
{
title: "操作",
key: "actions",
render: (_: any, task: RatioTaskItem) => (
<div className="flex items-center gap-2">
{operations.map((op) => (
<Tooltip key={op.key} title={op.label}>
<Button
type="text"
icon={op.icon}
onClick={() => op.onClick(task.id)}
/>
</Tooltip>
))}
</div>
),
},
];
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 = [
{
key: "delete",
label: "删除",
danger: true,
confirm: {
title: "确认删除该数据集?",
description: "删除后该数据集将无法恢复,请谨慎操作。",
okText: "删除",
cancelText: "取消",
okType: "danger",
},
icon: <DeleteOutlined />,
onClick: (item) => handleDelete(String(item.id)),
}
];
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.created_at || "-",
},
],
status: getStatusBadge(task.status),
}))}
pagination={pagination}
operations={operations}
/>
);
// 搜索、筛选和视图控制相关
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 (
<div className="">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold"></h2>
<Button
type="primary"
onClick={() => navigate("/data/synthesis/ratio-task/create")}
icon={<Plus className="w-4 h-4" />}
>
</Button>
</div>
<>
{/* 搜索、筛选和视图控制 */}
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={(keyword) => setSearchParams({ ...searchParams, keyword })}
searchPlaceholder="搜索任务名称"
filters={searchFilters}
onFiltersChange={handleSearchControlsFiltersChange}
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
viewMode={viewMode === "card" ? "card" : "list"}
onViewModeChange={handleViewModeChange}
showViewToggle={true}
/>
{/* 任务列表 */}
{viewMode === "list" ? renderTableView() : renderCardView()}
</>
</div>
);
}

View File

@@ -1,382 +0,0 @@
import { useState } from "react";
import {
Button,
Card,
Input,
Select,
Badge,
Progress,
Table,
Alert,
} from "antd";
import {
Plus,
Eye,
Clock,
Play,
CheckCircle,
AlertCircle,
Pause,
Download as DownloadIcon,
BarChart3,
} from "lucide-react";
import type { RatioTask } from "@/pages/RatioTask/ratio";
import { mockRatioTasks } from "@/mock/ratio";
import { useNavigate } from "react-router";
import CardView from "@/components/CardView";
import { SearchControls } from "@/components/SearchControls";
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
export default function RatioTasksPage() {
return <DevelopmentInProgress showTime="2025.11.30" />;
const navigate = useNavigate();
const [searchQuery, setSearchQuery] = useState("");
const [filterStatus, setFilterStatus] = useState("all");
const [filterType, setFilterType] = useState("all");
const [sortBy, setSortBy] = useState("createdAt");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
const [viewMode, setViewMode] = useState<"card" | "list">("card");
const [tasks, setTasks] = useState<RatioTask[]>(mockRatioTasks);
// 过滤和排序任务
const filteredAndSortedTasks = tasks
.filter((task) => {
const matchesSearch = task.name
.toLowerCase()
.includes(searchQuery.toLowerCase());
const matchesStatus =
filterStatus === "all" || task.status === filterStatus;
const matchesType = filterType === "all" || task.ratioType === filterType;
return matchesSearch && matchesStatus && matchesType;
})
.sort((a, b) => {
let aValue: any, bValue: any;
switch (sortBy) {
case "name":
aValue = a.name.toLowerCase();
bValue = b.name.toLowerCase();
break;
case "targetCount":
aValue = a.targetCount;
bValue = b.targetCount;
break;
case "generatedCount":
aValue = a.generatedCount;
bValue = b.generatedCount;
break;
case "progress":
aValue = a.progress;
bValue = b.progress;
break;
case "createdAt":
default:
aValue = new Date(a.createdAt).getTime();
bValue = new Date(b.createdAt).getTime();
break;
}
if (sortOrder === "asc") {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
const getStatusBadge = (status: string) => {
const statusConfig = {
pending: {
label: "等待中",
color: "#f09e10ff",
icon: <Clock className="w-4 h-4 inline mr-1" />,
},
running: {
label: "运行中",
color: "#007bff",
icon: <Play className="w-4 h-4 inline mr-1" />,
},
completed: {
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[status as keyof typeof statusConfig] || statusConfig.pending
);
};
const handleTaskAction = (taskId: number, action: string) => {
setTasks((prev) =>
prev.map((task) => {
if (task.id === taskId) {
switch (action) {
case "pause":
return { ...task, status: "paused" as const };
case "resume":
return { ...task, status: "running" as const };
case "stop":
return {
...task,
status: "failed" as const,
progress: task.progress,
};
default:
return task;
}
}
return task;
})
);
};
const columns = [
{
title: "任务名称",
dataIndex: "name",
key: "name",
},
{
title: "状态",
dataIndex: "status",
key: "status",
},
{
title: "配比方式",
dataIndex: "ratioType",
key: "ratioType",
},
{
title: "进度",
dataIndex: "progress",
key: "progress",
},
{
title: "目标数量",
dataIndex: "targetCount",
key: "targetCount",
},
{
title: "已生成",
dataIndex: "generatedCount",
key: "generatedCount",
},
{
title: "数据源",
dataIndex: "sourceDatasets",
key: "sourceDatasets",
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
},
{
title: "操作",
key: "actions",
render: (_: any, task: RatioTask) => (
<div className="flex items-center gap-1 justify-end">
{task.status === "running" && (
<Button
type="link"
size="small"
onClick={() => handleTaskAction(task.id, "pause")}
>
</Button>
)}
{task.status === "paused" && (
<Button
size="small"
type="link"
onClick={() => handleTaskAction(task.id, "resume")}
>
</Button>
)}
<Button type="link" size="small">
</Button>
</div>
),
},
];
const renderTableView = () => (
<Card>
<Table
columns={columns}
dataSource={filteredAndSortedTasks}
rowKey="id"
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">
{searchQuery || filterStatus !== "all" || filterType !== "all"
? "没有找到匹配的任务"
: "开始创建您的第一个配比任务"}
</p>
{!searchQuery &&
filterStatus === "all" &&
filterType === "all" && (
<Button
onClick={() =>
navigate("/data/synthesis/ratio-task/create")
}
type="primary"
>
<Plus className="w-4 h-4 mr-2" />
</Button>
)}
</div>
),
}}
/>
</Card>
);
const renderCardView = () => (
<CardView
data={filteredAndSortedTasks.map((task) => ({
...task,
description:
task.ratioType === "dataset" ? "按数据集配比" : "按标签配比",
icon: <BarChart3 className="w-6 h-6" />,
iconColor:
task.ratioType === "dataset" ? "bg-blue-100" : "bg-green-100",
statistics: [
{
label: "目标数量",
value: task.targetCount.toLocaleString(),
},
{
label: "已生成",
value: task.generatedCount.toLocaleString(),
},
{
label: "进度",
value: `${Math.round(task.progress)}%`,
},
],
status: getStatusBadge(task.status),
}))}
operations={[
{
key: "view",
label: "查看",
onClick: (item) => navigate(`/data/synthesis/ratio-task/${item.id}`),
},
{
key: "download",
label: "下载",
onClick: (item) => console.log("下载", item.name),
},
]}
/>
);
// 搜索、筛选和视图控制相关
const searchFilters = [
{
key: "status",
label: "状态筛选",
options: [
{ label: "全部状态", value: "all" },
{ label: "等待中", value: "pending" },
{ label: "运行中", value: "running" },
{ label: "已完成", value: "completed" },
{ label: "失败", value: "failed" },
{ label: "已暂停", value: "paused" },
],
},
{
key: "type",
label: "类型筛选",
options: [
{ label: "全部类型", value: "all" },
{ label: "按数据集", value: "dataset" },
{ label: "按标签", value: "label" },
],
},
{
key: "sortBy",
label: "排序方式",
options: [
{ label: "创建时间", value: "createdAt" },
{ label: "任务名称", value: "name" },
{ label: "目标数量", value: "targetCount" },
{ label: "已生成", value: "generatedCount" },
{ label: "进度", value: "progress" },
],
},
{
key: "sortOrder",
label: "排序顺序",
options: [
{ label: "升序", value: "asc" },
{ label: "降序", value: "desc" },
],
},
];
// 处理 SearchControls 的筛选变化
const handleSearchControlsFiltersChange = (
filters: Record<string, string[]>
) => {
setFilterStatus(filters.status?.[0] || "all");
setFilterType(filters.type?.[0] || "all");
setSortBy(filters.sortBy?.[0] || "createdAt");
setSortOrder((filters.sortOrder?.[0] as "asc" | "desc") || "desc");
};
// 处理视图切换
const handleViewModeChange = (mode: "card" | "list") => {
setViewMode(mode === "card" ? "card" : "list");
};
return (
<div className="">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold"></h2>
<Button
type="primary"
onClick={() => navigate("/data/synthesis/ratio-task/create")}
icon={<Plus className="w-4 h-4" />}
>
</Button>
</div>
<>
{/* 搜索、筛选和视图控制 */}
<SearchControls
searchTerm={searchQuery}
onSearchChange={setSearchQuery}
searchPlaceholder="搜索任务名称"
filters={searchFilters}
onFiltersChange={handleSearchControlsFiltersChange}
viewMode={viewMode === "card" ? "card" : "list"}
onViewModeChange={handleViewModeChange}
showViewToggle={true}
/>
{/* 任务列表 */}
{viewMode === "list" ? renderTableView() : renderCardView()}
</>
</div>
);
}

View File

@@ -0,0 +1,18 @@
import { get, post, put, del, download } from "@/utils/request";
// 查询配比任务列表(分页)
export function queryRatioTasksUsingGet(params?: any) {
return get("/api/synthesis/ratio-task", params);
}
// 创建配比任务
export function createRatioTaskUsingPost(data: any) {
return post("/api/synthesis/ratio-task", data);
}
// 删除配比任务(支持批量)
export function deleteRatioTasksUsingDelete(ids: string[]) {
const qs = (ids || []).map((id) => `ids=${encodeURIComponent(id)}`).join("&");
const url = qs ? `/api/synthesis/ratio-task?${qs}` : "/api/synthesis/ratio-task";
return del(url);
}

View File

@@ -1,24 +0,0 @@
export interface RatioTask {
id: number
name: string
status: "pending" | "running" | "completed" | "failed" | "paused"
progress: number
sourceDatasets: string[]
targetCount: number
generatedCount: number
createdAt: string
ratioType: "dataset" | "label"
estimatedTime?: string
quality?: number
errorMessage?: string
ratioConfigs: RatioConfig[]
}
export interface RatioConfig {
id: string
name: string
type: "dataset" | "label"
quantity: number
percentage: number
source: string
}

View File

@@ -0,0 +1,82 @@
// 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"
// 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
}
// 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
}
// API DTOs
export interface RatioConfigItem {
datasetId: string
counts: string
filter_conditions: string
}
export interface CreateRatioTaskRequest {
name: string
description?: string
totals: string
ratio_method: RatioMethod
config: RatioConfigItem[]
}
export interface TargetDatasetInfo {
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
}
export interface RatioTaskItem {
id: string
name: string
description?: string
status?: string
totals?: number
ratio_method?: RatioMethod
target_dataset_id?: string
target_dataset_name?: string
created_at?: string
updated_at?: string
}