You've already forked DataMate
feature: 数据配比增加通过更新时间来配置 (#95)
* feature: 数据配比增加通过更新时间来配置 * fix: 修复配比时间参数传递的问题
This commit is contained in:
@@ -1,7 +1,19 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Badge, Card, Input, Progress, Button, Divider } from "antd";
|
||||
import { BarChart3 } from "lucide-react";
|
||||
import { Badge, Card, Input, Progress, Button, DatePicker, Select } from "antd";
|
||||
import { BarChart3, Filter, Clock } from "lucide-react";
|
||||
import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
const { Option } = Select;
|
||||
|
||||
const TIME_RANGE_OPTIONS = [
|
||||
{ label: '最近1天', value: 1 },
|
||||
{ label: '最近3天', value: 3 },
|
||||
{ label: '最近7天', value: 7 },
|
||||
{ label: '最近15天', value: 15 },
|
||||
{ label: '最近30天', value: 30 },
|
||||
];
|
||||
|
||||
interface RatioConfigItem {
|
||||
id: string;
|
||||
@@ -10,6 +22,8 @@ interface RatioConfigItem {
|
||||
quantity: number;
|
||||
percentage: number;
|
||||
source: string;
|
||||
labelFilter?: string;
|
||||
dateRange?: string;
|
||||
}
|
||||
|
||||
interface RatioConfigProps {
|
||||
@@ -22,14 +36,18 @@ interface RatioConfigProps {
|
||||
}
|
||||
|
||||
const RatioConfig: React.FC<RatioConfigProps> = ({
|
||||
ratioType,
|
||||
selectedDatasets,
|
||||
datasets,
|
||||
totalTargetCount,
|
||||
distributions,
|
||||
onChange,
|
||||
}) => {
|
||||
ratioType,
|
||||
selectedDatasets,
|
||||
datasets,
|
||||
totalTargetCount,
|
||||
distributions,
|
||||
onChange,
|
||||
}) => {
|
||||
const [ratioConfigs, setRatioConfigs] = useState<RatioConfigItem[]>([]);
|
||||
const [datasetFilters, setDatasetFilters] = useState<Record<string, {
|
||||
labelFilter?: string;
|
||||
dateRange?: string;
|
||||
}>>({});
|
||||
|
||||
// 配比项总数
|
||||
const totalConfigured = useMemo(
|
||||
@@ -37,6 +55,36 @@ const RatioConfig: React.FC<RatioConfigProps> = ({
|
||||
[ratioConfigs]
|
||||
);
|
||||
|
||||
// 获取数据集的标签列表
|
||||
const getDatasetLabels = (datasetId: string): string[] => {
|
||||
const dist = distributions[String(datasetId)] || {};
|
||||
return Object.keys(dist);
|
||||
};
|
||||
|
||||
// 自动平均分配
|
||||
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,
|
||||
labelFilter: datasetFilters[datasetId]?.labelFilter,
|
||||
dateRange: datasetFilters[datasetId]?.dateRange,
|
||||
};
|
||||
});
|
||||
setRatioConfigs(newConfigs);
|
||||
onChange?.(newConfigs);
|
||||
};
|
||||
|
||||
// 更新数据集配比项
|
||||
const updateDatasetQuantity = (datasetId: string, quantity: number) => {
|
||||
setRatioConfigs((prev) => {
|
||||
@@ -55,6 +103,8 @@ const RatioConfig: React.FC<RatioConfigProps> = ({
|
||||
quantity: Math.min(quantity, totalTargetCount - totalOtherQuantity),
|
||||
percentage: Math.round((quantity / totalTargetCount) * 100),
|
||||
source: datasetId,
|
||||
labelFilter: datasetFilters[datasetId]?.labelFilter,
|
||||
dateRange: datasetFilters[datasetId]?.dateRange,
|
||||
};
|
||||
|
||||
let newConfigs;
|
||||
@@ -69,78 +119,85 @@ const RatioConfig: React.FC<RatioConfigProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
// 自动平均分配
|
||||
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];
|
||||
// 更新筛选条件
|
||||
const updateFilters = (datasetId: string, updates: {
|
||||
labelFilter?: string;
|
||||
dateRange?: [string, string];
|
||||
}) => {
|
||||
setDatasetFilters(prev => ({
|
||||
...prev,
|
||||
[datasetId]: {
|
||||
...prev[datasetId],
|
||||
...updates,
|
||||
}
|
||||
onChange?.(newConfigs);
|
||||
return newConfigs;
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
// 选中数据集变化时,移除未选中的配比项
|
||||
// 渲染筛选器
|
||||
const renderFilters = (datasetId: string) => {
|
||||
const labels = getDatasetLabels(datasetId);
|
||||
const config = ratioConfigs.find(c => c.source === datasetId);
|
||||
const filters = datasetFilters[datasetId] || {};
|
||||
|
||||
return (
|
||||
<div className="mb-3 p-2 bg-gray-50 rounded">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Filter size={14} className="text-gray-400" />
|
||||
<span className="text-xs font-medium">筛选条件</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mb-1">标签筛选</div>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder="选择标签"
|
||||
value={filters.labelFilter}
|
||||
onChange={(value) => updateFilters(datasetId, { labelFilter: value })}
|
||||
allowClear
|
||||
onClear={() => updateFilters(datasetId, { labelFilter: undefined })}
|
||||
>
|
||||
{labels.map(label => (
|
||||
<Option key={label} value={label}>{label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mb-1">标签更新时间</div>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder="选择标签更新时间"
|
||||
value={filters.dateRange}
|
||||
onChange={(dates) => updateFilters(datasetId, { dateRange: dates })}
|
||||
allowClear
|
||||
onClear={() => updateFilters(datasetId, { dateRange: undefined })}
|
||||
>
|
||||
{TIME_RANGE_OPTIONS.map(option => (
|
||||
<Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 选中数据集变化时,初始化筛选条件
|
||||
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;
|
||||
const initialFilters: Record<string, any> = {};
|
||||
selectedDatasets.forEach(datasetId => {
|
||||
const config = ratioConfigs.find(c => c.source === datasetId);
|
||||
if (config) {
|
||||
initialFilters[datasetId] = {
|
||||
labelFilter: config.labelFilter,
|
||||
dateRange: config.dateRange,
|
||||
};
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line
|
||||
setDatasetFilters(prev => ({ ...prev, ...initialFilters }));
|
||||
}, [selectedDatasets]);
|
||||
|
||||
return (
|
||||
@@ -148,7 +205,7 @@ const RatioConfig: React.FC<RatioConfigProps> = ({
|
||||
<div className="flex items-center justify-between p-4 border-bottom">
|
||||
<span className="text-sm font-bold">
|
||||
配比配置
|
||||
<span className="text-xs text-gray-500">
|
||||
<span className="text-xs text-gray-500 ml-1">
|
||||
(已配置:{totalConfigured}/{totalTargetCount}条)
|
||||
</span>
|
||||
</span>
|
||||
@@ -170,41 +227,36 @@ const RatioConfig: React.FC<RatioConfigProps> = ({
|
||||
<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 className="p-3 bg-gray-50 rounded-lg mb-4">
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-auto">
|
||||
|
||||
<div className="flex-1 overflow-auto space-y-4">
|
||||
{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">
|
||||
<Card key={datasetId} size="small" className="mb-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">
|
||||
@@ -216,97 +268,36 @@ const RatioConfig: React.FC<RatioConfigProps> = ({
|
||||
{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) =>
|
||||
updateDatasetQuantity(
|
||||
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) =>
|
||||
updateLabelQuantity(
|
||||
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>
|
||||
|
||||
{/* 筛选条件 */}
|
||||
{renderFilters(datasetId)}
|
||||
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs">数量:</span>
|
||||
<Input
|
||||
type="number"
|
||||
value={currentQuantity}
|
||||
onChange={(e) =>
|
||||
updateDatasetQuantity(
|
||||
datasetId,
|
||||
Number(e.target.value)
|
||||
)
|
||||
}
|
||||
style={{ width: 100 }}
|
||||
min={0}
|
||||
max={Math.min(
|
||||
dataset.fileCount || 0,
|
||||
totalTargetCount
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">条</span>
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round(
|
||||
(currentQuantity / totalTargetCount) * 100
|
||||
)}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user