Revert "feat: fix the problem in the Operator Market frontend pages"

This commit is contained in:
Kecheng Sha
2025-12-29 12:00:37 +08:00
committed by GitHub
parent 8f30f71a68
commit 0df7a872e4
213 changed files with 45537 additions and 45547 deletions

View File

@@ -1,155 +1,155 @@
import { useState } from "react";
import { Button, Form, message } from "antd";
import { ArrowLeft, ChevronRight } from "lucide-react";
import { createRatioTaskUsingPost } from "@/pages/RatioTask/ratio.api.ts";
import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
import { useNavigate } from "react-router";
import SelectDataset from "@/pages/RatioTask/Create/components/SelectDataset.tsx";
import BasicInformation from "@/pages/RatioTask/Create/components/BasicInformation.tsx";
import RatioConfig from "@/pages/RatioTask/Create/components/RatioConfig.tsx";
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;
}
const totals = String(values.totalTargetCount);
const config = ratioTaskForm.ratioConfigs.map((c) => {
return {
datasetId: c.source,
counts: String(c.quantity ?? 0),
filterConditions: { label: c.labelFilter, dateRange: String(c.dateRange ?? 0)},
};
});
setCreating(true);
await createRatioTaskUsingPost({
name: values.name,
description: values.description,
totals,
config,
});
message.success("配比任务创建成功");
navigate("/data/synthesis/ratio-task");
} catch {
message.error("配比任务创建失败,请重试");
} finally {
setCreating(false);
}
};
const handleValuesChange = (_, allValues) => {
setRatioTaskForm({ ...ratioTaskForm, ...allValues });
};
return (
<div className="h-full flex flex-col gap-4">
{/* Header */}
<div className="flex items-center justify-between">
<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>
<div className="h-full flex-overflow-auto border-card">
<div className="h-full overflow-auto p-6">
<Form
form={form}
initialValues={ratioTaskForm}
onValuesChange={handleValuesChange}
layout="vertical"
className="h-full"
>
<BasicInformation
totalTargetCount={ratioTaskForm.totalTargetCount}
/>
<div className="flex h-full">
<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)}
/>
<ChevronRight className="self-center" />
<RatioConfig
ratioType={ratioTaskForm.ratioType}
selectedDatasets={ratioTaskForm.selectedDatasets}
datasets={datasets}
totalTargetCount={ratioTaskForm.totalTargetCount}
distributions={distributions}
onChange={(configs) =>
setRatioTaskForm((prev) => ({
...prev,
ratioConfigs: configs,
}))
}
/>
</div>
</Form>
</div>
<div className="flex justify-end gap-2 p-6">
<Button onClick={() => navigate("/data/synthesis/ratio-task")}>
</Button>
<Button
type="primary"
onClick={handleCreateRatioTask}
loading={creating}
disabled={
!ratioTaskForm.name || ratioTaskForm.ratioConfigs.length === 0
}
>
</Button>
</div>
</div>
</div>
);
}
import { useState } from "react";
import { Button, Form, message } from "antd";
import { ArrowLeft, ChevronRight } from "lucide-react";
import { createRatioTaskUsingPost } from "@/pages/RatioTask/ratio.api.ts";
import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
import { useNavigate } from "react-router";
import SelectDataset from "@/pages/RatioTask/Create/components/SelectDataset.tsx";
import BasicInformation from "@/pages/RatioTask/Create/components/BasicInformation.tsx";
import RatioConfig from "@/pages/RatioTask/Create/components/RatioConfig.tsx";
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;
}
const totals = String(values.totalTargetCount);
const config = ratioTaskForm.ratioConfigs.map((c) => {
return {
datasetId: c.source,
counts: String(c.quantity ?? 0),
filterConditions: { label: c.labelFilter, dateRange: String(c.dateRange ?? 0)},
};
});
setCreating(true);
await createRatioTaskUsingPost({
name: values.name,
description: values.description,
totals,
config,
});
message.success("配比任务创建成功");
navigate("/data/synthesis/ratio-task");
} catch {
message.error("配比任务创建失败,请重试");
} finally {
setCreating(false);
}
};
const handleValuesChange = (_, allValues) => {
setRatioTaskForm({ ...ratioTaskForm, ...allValues });
};
return (
<div className="h-full flex flex-col gap-4">
{/* Header */}
<div className="flex items-center justify-between">
<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>
<div className="h-full flex-overflow-auto border-card">
<div className="h-full overflow-auto p-6">
<Form
form={form}
initialValues={ratioTaskForm}
onValuesChange={handleValuesChange}
layout="vertical"
className="h-full"
>
<BasicInformation
totalTargetCount={ratioTaskForm.totalTargetCount}
/>
<div className="flex h-full">
<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)}
/>
<ChevronRight className="self-center" />
<RatioConfig
ratioType={ratioTaskForm.ratioType}
selectedDatasets={ratioTaskForm.selectedDatasets}
datasets={datasets}
totalTargetCount={ratioTaskForm.totalTargetCount}
distributions={distributions}
onChange={(configs) =>
setRatioTaskForm((prev) => ({
...prev,
ratioConfigs: configs,
}))
}
/>
</div>
</Form>
</div>
<div className="flex justify-end gap-2 p-6">
<Button onClick={() => navigate("/data/synthesis/ratio-task")}>
</Button>
<Button
type="primary"
onClick={handleCreateRatioTask}
loading={creating}
disabled={
!ratioTaskForm.name || ratioTaskForm.ratioConfigs.length === 0
}
>
</Button>
</div>
</div>
</div>
);
}

View File

@@ -1,36 +1,36 @@
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-2">
<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;
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-2">
<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

@@ -1,384 +1,384 @@
import React, { useMemo, useState, useEffect, FC } from "react";
import {
Badge,
Card,
Button,
Select,
Table,
InputNumber,
} from "antd";
import { BarChart3 } from "lucide-react";
import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
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 LabelFilter {
label: string;
value: string;
}
interface RatioConfigItem {
id: string;
name: string;
type: "dataset" | "label";
quantity: number;
percentage: number;
source: string; // dataset id
labelFilter?: LabelFilter;
dateRange?: number;
}
interface RatioConfigProps {
ratioType: "dataset" | "label";
selectedDatasets: string[];
datasets: Dataset[];
totalTargetCount: number;
// distributions now: { datasetId: { labelName: { labelValue: count } } }
distributions: Record<string, Record<string, Record<string, number>>>;
onChange?: (configs: RatioConfigItem[]) => void;
}
const genId = (datasetId: string) =>
`${datasetId}-${Math.random().toString(36).slice(2, 9)}`;
const RatioConfig: FC<RatioConfigProps> = ({
ratioType,
selectedDatasets,
datasets,
totalTargetCount,
distributions,
onChange,
}) => {
const [ratioConfigs, setRatioConfigs] = useState<RatioConfigItem[]>([]);
const totalConfigured = useMemo(
() => ratioConfigs.reduce((sum, c) => sum + (c.quantity || 0), 0),
[ratioConfigs]
);
const getDatasetLabels = (datasetId: string): string[] => {
const dist = distributions[String(datasetId)] || {};
return Object.keys(dist);
};
const getLabelValues = (datasetId: string, label: string): string[] => {
return Object.keys(distributions[String(datasetId)]?.[label] || {});
};
const addConfig = (datasetId: string) => {
const dataset = datasets.find((d) => String(d.id) === datasetId);
const newConfig: RatioConfigItem = {
id: genId(datasetId),
name: dataset?.name || datasetId,
type: ratioType,
quantity: 0,
percentage: 0,
source: datasetId,
};
const newConfigs = [...ratioConfigs, newConfig];
setRatioConfigs(newConfigs);
onChange?.(newConfigs);
};
const removeConfig = (configId: string) => {
const newConfigs = ratioConfigs.filter((c) => c.id !== configId);
const adjusted = recomputePercentages(newConfigs);
setRatioConfigs(adjusted);
onChange?.(adjusted);
};
const updateConfig = (
configId: string,
updates: Partial<
Pick<RatioConfigItem, "quantity" | "labelFilter" | "dateRange">
>
) => {
const newConfigs = ratioConfigs.map((c) =>
c.id === configId ? { ...c, ...updates } : c
);
const adjusted = recomputePercentages(newConfigs);
setRatioConfigs(adjusted);
onChange?.(adjusted);
};
const recomputePercentages = (configs: RatioConfigItem[]) => {
return configs.map((c) => ({
...c,
percentage:
totalTargetCount > 0
? Math.round((c.quantity / totalTargetCount) * 100)
: 0,
}));
};
const generateAutoRatio = () => {
const selectedCount = selectedDatasets.length;
if (selectedCount === 0) return;
const baseQuantity = Math.floor(totalTargetCount / selectedCount);
const remainder = totalTargetCount % selectedCount;
let newConfigs: RatioConfigItem[] = ratioConfigs.filter(
(c) => !selectedDatasets.includes(c.source)
);
selectedDatasets.forEach((datasetId, index) => {
const dataset = datasets.find((d) => String(d.id) === datasetId);
const quantity = baseQuantity + (index < remainder ? 1 : 0);
const config: RatioConfigItem = {
id: genId(datasetId),
name: dataset?.name || datasetId,
type: ratioType,
quantity,
percentage: Math.round((quantity / totalTargetCount) * 100),
source: datasetId,
};
newConfigs.push(config);
});
setRatioConfigs(newConfigs);
onChange?.(newConfigs);
};
useEffect(() => {
const keep = ratioConfigs.filter((c) =>
selectedDatasets.includes(c.source)
);
if (keep.length !== ratioConfigs.length) {
const adjusted = recomputePercentages(keep);
setRatioConfigs(adjusted);
onChange?.(adjusted);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDatasets]);
return (
<div className="border-card flex-1 flex flex-col min-w-[320px]">
<div className="flex items-center justify-between p-4 border-bottom">
<span className="text-sm font-bold">
<span className="text-xs text-gray-500 ml-1">
(:{totalConfigured}/{totalTargetCount})
</span>
</span>
<div className="flex items-center gap-2">
<Button
type="link"
size="small"
onClick={generateAutoRatio}
disabled={selectedDatasets.length === 0}
>
</Button>
</div>
</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 className="flex-overflow-auto gap-4 p-4">
{ratioConfigs.length > 0 && (
<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 space-y-4">
{selectedDatasets.map((datasetId) => {
const dataset = datasets.find((d) => String(d.id) === datasetId);
if (!dataset) return null;
const datasetConfigs = ratioConfigs.filter(
(c) => c.source === datasetId
);
const labels = getDatasetLabels(datasetId);
// helper: used values per label for this dataset (exclude a given row when needed)
const getUsedValuesForLabel = (label: string, excludeId?: string) => {
return new Set(
datasetConfigs
.filter((c) => c.id !== excludeId && c.labelFilter?.label === label)
.map((c) => c.labelFilter?.value)
.filter(Boolean) as string[]
);
};
const columns = [
{
title: "标签",
dataIndex: "labelFilter",
key: "labelFilter",
render: (_: any, record: RatioConfigItem) => {
const availableLabels = labels
.map((l) => ({
label: l,
value: l,
disabled: getLabelValues(datasetId, l).every((v) => getUsedValuesForLabel(l, record.id).has(v)),
}))
return (
<Select
style={{ width: "160px" }}
placeholder="选择标签"
value={record.labelFilter?.label}
options={availableLabels}
allowClear
onChange={(value) => {
if (!value) {
updateConfig(record.id, { labelFilter: undefined });
} else {
// reset value when label changes
updateConfig(record.id, {
labelFilter: { label: value, value: "" },
});
}
}}
/>
);
},
},
{
title: "标签值",
dataIndex: "labelValue",
key: "labelValue",
render: (_: any, record: RatioConfigItem) => {
const selectedLabel = record.labelFilter?.label;
const options = selectedLabel
? getLabelValues(datasetId, selectedLabel).map((v) => ({
label: v,
value: v,
disabled: datasetConfigs.some(
(c) =>
c.id !== record.id &&
c.labelFilter?.label === selectedLabel &&
c.labelFilter?.value === v
),
}))
: [];
return (
<Select
style={{ width: "180px" }}
placeholder="选择标签值"
value={record.labelFilter?.value || undefined}
options={options}
allowClear
disabled={!selectedLabel}
onChange={(value) => {
if (!selectedLabel) return;
updateConfig(record.id, {
labelFilter: {
label: selectedLabel,
value: value || "",
},
});
}}
/>
);
},
},
{
title: "标签更新时间",
dataIndex: "dateRange",
key: "dateRange",
render: (_: any, record: RatioConfigItem) => (
<Select
style={{ width: "140px" }}
placeholder="选择标签更新时间"
value={record.dateRange}
options={TIME_RANGE_OPTIONS}
allowClear
onChange={(value) =>
updateConfig(record.id, {
dateRange: value || undefined,
})
}
/>
),
},
{
title: "数量",
dataIndex: "quantity",
key: "quantity",
render: (_: any, record: RatioConfigItem) => (
<InputNumber
min={0}
max={Math.min(dataset.fileCount || 0, totalTargetCount)}
value={record.quantity}
onChange={(v) =>
updateConfig(record.id, { quantity: Number(v || 0) })
}
/>
),
},
{
title: "操作",
dataIndex: "actions",
key: "actions",
render: (_: any, record: RatioConfigItem) => (
<Button danger size="small" onClick={() => removeConfig(record.id)}>
</Button>
),
},
];
return (
<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">{dataset.name}</span>
<Badge color="gray">{dataset.fileCount}</Badge>
</div>
<div className="text-xs text-gray-500">
{datasetConfigs.reduce((s, c) => s + (c.percentage || 0), 0)}%
</div>
</div>
<Table
dataSource={datasetConfigs}
columns={columns}
pagination={false}
rowKey="id"
size="small"
locale={{ emptyText: "暂无配比项,请添加" }}
/>
<div className="flex justify-end mt-3">
<Button size="small" onClick={() => addConfig(datasetId)}>
</Button>
</div>
</Card>
);
})}
</div>
</div>
)}
</div>
);
};
export default RatioConfig;
import React, { useMemo, useState, useEffect, FC } from "react";
import {
Badge,
Card,
Button,
Select,
Table,
InputNumber,
} from "antd";
import { BarChart3 } from "lucide-react";
import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
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 LabelFilter {
label: string;
value: string;
}
interface RatioConfigItem {
id: string;
name: string;
type: "dataset" | "label";
quantity: number;
percentage: number;
source: string; // dataset id
labelFilter?: LabelFilter;
dateRange?: number;
}
interface RatioConfigProps {
ratioType: "dataset" | "label";
selectedDatasets: string[];
datasets: Dataset[];
totalTargetCount: number;
// distributions now: { datasetId: { labelName: { labelValue: count } } }
distributions: Record<string, Record<string, Record<string, number>>>;
onChange?: (configs: RatioConfigItem[]) => void;
}
const genId = (datasetId: string) =>
`${datasetId}-${Math.random().toString(36).slice(2, 9)}`;
const RatioConfig: FC<RatioConfigProps> = ({
ratioType,
selectedDatasets,
datasets,
totalTargetCount,
distributions,
onChange,
}) => {
const [ratioConfigs, setRatioConfigs] = useState<RatioConfigItem[]>([]);
const totalConfigured = useMemo(
() => ratioConfigs.reduce((sum, c) => sum + (c.quantity || 0), 0),
[ratioConfigs]
);
const getDatasetLabels = (datasetId: string): string[] => {
const dist = distributions[String(datasetId)] || {};
return Object.keys(dist);
};
const getLabelValues = (datasetId: string, label: string): string[] => {
return Object.keys(distributions[String(datasetId)]?.[label] || {});
};
const addConfig = (datasetId: string) => {
const dataset = datasets.find((d) => String(d.id) === datasetId);
const newConfig: RatioConfigItem = {
id: genId(datasetId),
name: dataset?.name || datasetId,
type: ratioType,
quantity: 0,
percentage: 0,
source: datasetId,
};
const newConfigs = [...ratioConfigs, newConfig];
setRatioConfigs(newConfigs);
onChange?.(newConfigs);
};
const removeConfig = (configId: string) => {
const newConfigs = ratioConfigs.filter((c) => c.id !== configId);
const adjusted = recomputePercentages(newConfigs);
setRatioConfigs(adjusted);
onChange?.(adjusted);
};
const updateConfig = (
configId: string,
updates: Partial<
Pick<RatioConfigItem, "quantity" | "labelFilter" | "dateRange">
>
) => {
const newConfigs = ratioConfigs.map((c) =>
c.id === configId ? { ...c, ...updates } : c
);
const adjusted = recomputePercentages(newConfigs);
setRatioConfigs(adjusted);
onChange?.(adjusted);
};
const recomputePercentages = (configs: RatioConfigItem[]) => {
return configs.map((c) => ({
...c,
percentage:
totalTargetCount > 0
? Math.round((c.quantity / totalTargetCount) * 100)
: 0,
}));
};
const generateAutoRatio = () => {
const selectedCount = selectedDatasets.length;
if (selectedCount === 0) return;
const baseQuantity = Math.floor(totalTargetCount / selectedCount);
const remainder = totalTargetCount % selectedCount;
let newConfigs: RatioConfigItem[] = ratioConfigs.filter(
(c) => !selectedDatasets.includes(c.source)
);
selectedDatasets.forEach((datasetId, index) => {
const dataset = datasets.find((d) => String(d.id) === datasetId);
const quantity = baseQuantity + (index < remainder ? 1 : 0);
const config: RatioConfigItem = {
id: genId(datasetId),
name: dataset?.name || datasetId,
type: ratioType,
quantity,
percentage: Math.round((quantity / totalTargetCount) * 100),
source: datasetId,
};
newConfigs.push(config);
});
setRatioConfigs(newConfigs);
onChange?.(newConfigs);
};
useEffect(() => {
const keep = ratioConfigs.filter((c) =>
selectedDatasets.includes(c.source)
);
if (keep.length !== ratioConfigs.length) {
const adjusted = recomputePercentages(keep);
setRatioConfigs(adjusted);
onChange?.(adjusted);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDatasets]);
return (
<div className="border-card flex-1 flex flex-col min-w-[320px]">
<div className="flex items-center justify-between p-4 border-bottom">
<span className="text-sm font-bold">
<span className="text-xs text-gray-500 ml-1">
(:{totalConfigured}/{totalTargetCount})
</span>
</span>
<div className="flex items-center gap-2">
<Button
type="link"
size="small"
onClick={generateAutoRatio}
disabled={selectedDatasets.length === 0}
>
</Button>
</div>
</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 className="flex-overflow-auto gap-4 p-4">
{ratioConfigs.length > 0 && (
<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 space-y-4">
{selectedDatasets.map((datasetId) => {
const dataset = datasets.find((d) => String(d.id) === datasetId);
if (!dataset) return null;
const datasetConfigs = ratioConfigs.filter(
(c) => c.source === datasetId
);
const labels = getDatasetLabels(datasetId);
// helper: used values per label for this dataset (exclude a given row when needed)
const getUsedValuesForLabel = (label: string, excludeId?: string) => {
return new Set(
datasetConfigs
.filter((c) => c.id !== excludeId && c.labelFilter?.label === label)
.map((c) => c.labelFilter?.value)
.filter(Boolean) as string[]
);
};
const columns = [
{
title: "标签",
dataIndex: "labelFilter",
key: "labelFilter",
render: (_: any, record: RatioConfigItem) => {
const availableLabels = labels
.map((l) => ({
label: l,
value: l,
disabled: getLabelValues(datasetId, l).every((v) => getUsedValuesForLabel(l, record.id).has(v)),
}))
return (
<Select
style={{ width: "160px" }}
placeholder="选择标签"
value={record.labelFilter?.label}
options={availableLabels}
allowClear
onChange={(value) => {
if (!value) {
updateConfig(record.id, { labelFilter: undefined });
} else {
// reset value when label changes
updateConfig(record.id, {
labelFilter: { label: value, value: "" },
});
}
}}
/>
);
},
},
{
title: "标签值",
dataIndex: "labelValue",
key: "labelValue",
render: (_: any, record: RatioConfigItem) => {
const selectedLabel = record.labelFilter?.label;
const options = selectedLabel
? getLabelValues(datasetId, selectedLabel).map((v) => ({
label: v,
value: v,
disabled: datasetConfigs.some(
(c) =>
c.id !== record.id &&
c.labelFilter?.label === selectedLabel &&
c.labelFilter?.value === v
),
}))
: [];
return (
<Select
style={{ width: "180px" }}
placeholder="选择标签值"
value={record.labelFilter?.value || undefined}
options={options}
allowClear
disabled={!selectedLabel}
onChange={(value) => {
if (!selectedLabel) return;
updateConfig(record.id, {
labelFilter: {
label: selectedLabel,
value: value || "",
},
});
}}
/>
);
},
},
{
title: "标签更新时间",
dataIndex: "dateRange",
key: "dateRange",
render: (_: any, record: RatioConfigItem) => (
<Select
style={{ width: "140px" }}
placeholder="选择标签更新时间"
value={record.dateRange}
options={TIME_RANGE_OPTIONS}
allowClear
onChange={(value) =>
updateConfig(record.id, {
dateRange: value || undefined,
})
}
/>
),
},
{
title: "数量",
dataIndex: "quantity",
key: "quantity",
render: (_: any, record: RatioConfigItem) => (
<InputNumber
min={0}
max={Math.min(dataset.fileCount || 0, totalTargetCount)}
value={record.quantity}
onChange={(v) =>
updateConfig(record.id, { quantity: Number(v || 0) })
}
/>
),
},
{
title: "操作",
dataIndex: "actions",
key: "actions",
render: (_: any, record: RatioConfigItem) => (
<Button danger size="small" onClick={() => removeConfig(record.id)}>
</Button>
),
},
];
return (
<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">{dataset.name}</span>
<Badge color="gray">{dataset.fileCount}</Badge>
</div>
<div className="text-xs text-gray-500">
{datasetConfigs.reduce((s, c) => s + (c.percentage || 0), 0)}%
</div>
</div>
<Table
dataSource={datasetConfigs}
columns={columns}
pagination={false}
rowKey="id"
size="small"
locale={{ emptyText: "暂无配比项,请添加" }}
/>
<div className="flex justify-end mt-3">
<Button size="small" onClick={() => addConfig(datasetId)}>
</Button>
</div>
</Card>
);
})}
</div>
</div>
)}
</div>
);
};
export default RatioConfig;

View File

@@ -1,243 +1,243 @@
// typescript
import React, { useEffect, useState } from "react";
import { Badge, Button, Card, Checkbox, Input, Pagination } from "antd";
import { Search as SearchIcon } from "lucide-react";
import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
import {
queryDatasetsUsingGet,
queryDatasetByIdUsingGet,
} from "@/pages/DataManagement/dataset.api.ts";
interface SelectDatasetProps {
selectedDatasets: string[];
onSelectedDatasetsChange: (next: string[]) => void;
// distributions now: { datasetId: { labelName: { labelValue: count } } }
onDistributionsChange?: (
next: Record<string, Record<string, Record<string, number>>>
) => void;
onDatasetsChange?: (list: Dataset[]) => void;
}
const SelectDataset: React.FC<SelectDatasetProps> = ({
selectedDatasets,
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, Record<string, number>>>
>({});
// Helper: flatten nested distribution for preview and filter logic
const flattenDistribution = (
dist?: Record<string, Record<string, number>>
): Array<{ label: string; value: string; count: number }> => {
if (!dist) return [];
const items: Array<{ label: string; value: string; count: number }> = [];
Object.entries(dist).forEach(([label, values]) => {
if (values && typeof values === "object") {
Object.entries(values).forEach(([val, cnt]) => {
items.push({ label, value: val, count: cnt });
});
}
});
return items;
};
// 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().then(() => {});
}, [pagination.page, pagination.size, searchQuery]);
// Fetch label distributions when datasets change
useEffect(() => {
const fetchDistributions = async () => {
if (!datasets?.length) return;
const idsToFetch = datasets
.map((d) => String(d.id))
.filter((id) => !distributions[id]);
if (!idsToFetch.length) return;
try {
const next: Record<
string,
Record<string, Record<string, number>>
> = { ...distributions };
for (const id of idsToFetch) {
let dist: Record<string, Record<string, number>> | undefined =
undefined;
try {
const detRes = await queryDatasetByIdUsingGet(id);
const det = detRes?.data;
if (det) {
const picked = det?.distribution;
if (picked && typeof picked === "object") {
// Assume picked is now { labelName: { labelValue: count } }
dist = picked as Record<string, Record<string, number>>;
}
}
} catch {
dist = undefined;
}
next[String(id)] = dist || {};
}
setDistributions(next);
onDistributionsChange?.(next);
} catch {
// ignore
}
};
fetchDistributions().then(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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="border-card flex-1 flex flex-col min-w-[320px]">
<div className="flex items-center justify-between p-4 border-bottom">
<div className="flex items-center gap-4">
<span className="text-sm font-medium">
<span className="text-xs text-gray-500">
(: {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">
<Input
prefix={<SearchIcon className="text-gray-400" />}
placeholder="搜索数据集"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setPagination((p) => ({ ...p, page: 1 }));
}}
/>
<div className="flex-1 overflow-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);
const distFor = distributions[idStr];
const flat = flattenDistribution(distFor);
return (
<Card
key={dataset.id}
size="small"
className={`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>
<div className="mt-2">
{distFor ? (
flat.length > 0 ? (
<div className="flex flex-wrap gap-2 text-xs">
{flat.slice(0, 8).map((it) => (
<Badge
key={`${it.label}_${it.value}`}
color="gray"
>{`${it.label}/${it.value}: ${it.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">
<div className="flex items-center gap-3">
<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>
</div>
</div>
);
};
export default SelectDataset;
// typescript
import React, { useEffect, useState } from "react";
import { Badge, Button, Card, Checkbox, Input, Pagination } from "antd";
import { Search as SearchIcon } from "lucide-react";
import type { Dataset } from "@/pages/DataManagement/dataset.model.ts";
import {
queryDatasetsUsingGet,
queryDatasetByIdUsingGet,
} from "@/pages/DataManagement/dataset.api.ts";
interface SelectDatasetProps {
selectedDatasets: string[];
onSelectedDatasetsChange: (next: string[]) => void;
// distributions now: { datasetId: { labelName: { labelValue: count } } }
onDistributionsChange?: (
next: Record<string, Record<string, Record<string, number>>>
) => void;
onDatasetsChange?: (list: Dataset[]) => void;
}
const SelectDataset: React.FC<SelectDatasetProps> = ({
selectedDatasets,
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, Record<string, number>>>
>({});
// Helper: flatten nested distribution for preview and filter logic
const flattenDistribution = (
dist?: Record<string, Record<string, number>>
): Array<{ label: string; value: string; count: number }> => {
if (!dist) return [];
const items: Array<{ label: string; value: string; count: number }> = [];
Object.entries(dist).forEach(([label, values]) => {
if (values && typeof values === "object") {
Object.entries(values).forEach(([val, cnt]) => {
items.push({ label, value: val, count: cnt });
});
}
});
return items;
};
// 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().then(() => {});
}, [pagination.page, pagination.size, searchQuery]);
// Fetch label distributions when datasets change
useEffect(() => {
const fetchDistributions = async () => {
if (!datasets?.length) return;
const idsToFetch = datasets
.map((d) => String(d.id))
.filter((id) => !distributions[id]);
if (!idsToFetch.length) return;
try {
const next: Record<
string,
Record<string, Record<string, number>>
> = { ...distributions };
for (const id of idsToFetch) {
let dist: Record<string, Record<string, number>> | undefined =
undefined;
try {
const detRes = await queryDatasetByIdUsingGet(id);
const det = detRes?.data;
if (det) {
const picked = det?.distribution;
if (picked && typeof picked === "object") {
// Assume picked is now { labelName: { labelValue: count } }
dist = picked as Record<string, Record<string, number>>;
}
}
} catch {
dist = undefined;
}
next[String(id)] = dist || {};
}
setDistributions(next);
onDistributionsChange?.(next);
} catch {
// ignore
}
};
fetchDistributions().then(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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="border-card flex-1 flex flex-col min-w-[320px]">
<div className="flex items-center justify-between p-4 border-bottom">
<div className="flex items-center gap-4">
<span className="text-sm font-medium">
<span className="text-xs text-gray-500">
(: {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">
<Input
prefix={<SearchIcon className="text-gray-400" />}
placeholder="搜索数据集"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setPagination((p) => ({ ...p, page: 1 }));
}}
/>
<div className="flex-1 overflow-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);
const distFor = distributions[idStr];
const flat = flattenDistribution(distFor);
return (
<Card
key={dataset.id}
size="small"
className={`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>
<div className="mt-2">
{distFor ? (
flat.length > 0 ? (
<div className="flex flex-wrap gap-2 text-xs">
{flat.slice(0, 8).map((it) => (
<Badge
key={`${it.label}_${it.value}`}
color="gray"
>{`${it.label}/${it.value}: ${it.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">
<div className="flex items-center gap-3">
<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>
</div>
</div>
);
};
export default SelectDataset;

View File

@@ -1,58 +1,58 @@
import { Card } from "antd"
import { BarChart3, Database, Users, Zap } from "lucide-react"
const metrics = [
{
label: "总数据量",
value: "2.5M",
icon: Database,
change: "+12.5%",
color: "text-blue-400",
},
{
label: "配比成功率",
value: "94.2%",
icon: BarChart3,
change: "+2.1%",
color: "text-emerald-400",
},
{
label: "处理速度",
value: "185K/s",
icon: Zap,
change: "+8.3%",
color: "text-amber-400",
},
{
label: "活跃用户",
value: "156.8K",
icon: Users,
change: "+5.2%",
color: "text-purple-400",
},
]
export default function DataMetrics() {
return (
<div className="border-card grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mt-8">
{metrics.map((metric, idx) => {
const Icon = metric.icon
return (
<div
key={idx}
className="border-border bg-card/50 backdrop-blur p-4 hover:bg-card/70 transition-colors cursor-pointer"
>
<div className="flex items-start justify-between mb-3">
<div className={`p-2 rounded-lg bg-muted/50 ${metric.color}`}>
<Icon className="w-5 h-5" />
</div>
<span className="text-xs font-medium text-emerald-400">{metric.change}</span>
</div>
<p className="text-sm text-muted-foreground mb-1">{metric.label}</p>
<p className="text-2xl font-bold text-foreground">{metric.value}</p>
</div>
)
})}
</div>
)
}
import { Card } from "antd"
import { BarChart3, Database, Users, Zap } from "lucide-react"
const metrics = [
{
label: "总数据量",
value: "2.5M",
icon: Database,
change: "+12.5%",
color: "text-blue-400",
},
{
label: "配比成功率",
value: "94.2%",
icon: BarChart3,
change: "+2.1%",
color: "text-emerald-400",
},
{
label: "处理速度",
value: "185K/s",
icon: Zap,
change: "+8.3%",
color: "text-amber-400",
},
{
label: "活跃用户",
value: "156.8K",
icon: Users,
change: "+5.2%",
color: "text-purple-400",
},
]
export default function DataMetrics() {
return (
<div className="border-card grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mt-8">
{metrics.map((metric, idx) => {
const Icon = metric.icon
return (
<div
key={idx}
className="border-border bg-card/50 backdrop-blur p-4 hover:bg-card/70 transition-colors cursor-pointer"
>
<div className="flex items-start justify-between mb-3">
<div className={`p-2 rounded-lg bg-muted/50 ${metric.color}`}>
<Icon className="w-5 h-5" />
</div>
<span className="text-xs font-medium text-emerald-400">{metric.change}</span>
</div>
<p className="text-sm text-muted-foreground mb-1">{metric.label}</p>
<p className="text-2xl font-bold text-foreground">{metric.value}</p>
</div>
)
})}
</div>
)
}

View File

@@ -1,76 +1,76 @@
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
LineChart,
Line,
} from "recharts"
const chartData = [
{ name: "一月", 配比率: 65, 成功数: 2400, 失败数: 240 },
{ name: "二月", 配比率: 72, 成功数: 2210, 失败数: 221 },
{ name: "三月", 配比率: 78, 成功数: 2290, 失败数: 229 },
{ name: "四月", 配比率: 84, 成功数: 2000, 失败数: 200 },
{ name: "五月", 配比率: 90, 成功数: 2181, 失败数: 218 },
{ name: "六月", 配比率: 94, 成功数: 2500, 失败数: 250 },
]
export default function DataRatioChart() {
return (
<div className="lg:col-span-3 space-y-6">
<div className="border-border bg-card/50 backdrop-blur p-6">
<h3 className="text-base font-semibold text-foreground mb-4"></h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="rgb(var(--border))" />
<XAxis dataKey="name" stroke="rgb(var(--muted-foreground))" />
<YAxis stroke="rgb(var(--muted-foreground))" />
<Tooltip
contentStyle={{
backgroundColor: "rgb(var(--card))",
border: "1px solid rgb(var(--border))",
borderRadius: "8px",
}}
cursor={{ fill: "rgba(59, 130, 246, 0.1)" }}
/>
<Legend />
<Bar dataKey="成功数" stackId="a" fill="#3b82f6" radius={[8, 8, 0, 0]} />
<Bar dataKey="失败数" stackId="a" fill="#ef4444" radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
<div className="border-border bg-card/50 backdrop-blur p-6">
<h3 className="text-base font-semibold text-foreground mb-4">线</h3>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="rgb(var(--border))" />
<XAxis dataKey="name" stroke="rgb(var(--muted-foreground))" />
<YAxis stroke="rgb(var(--muted-foreground))" />
<Tooltip
contentStyle={{
backgroundColor: "rgb(var(--card))",
border: "1px solid rgb(var(--border))",
borderRadius: "8px",
}}
cursor={{ stroke: "rgba(34, 197, 94, 0.2)" }}
/>
<Line
type="monotone"
dataKey="配比率"
stroke="#22c55e"
strokeWidth={2}
dot={{ fill: "#22c55e", r: 4 }}
activeDot={{ r: 6 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
)
}
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
LineChart,
Line,
} from "recharts"
const chartData = [
{ name: "一月", 配比率: 65, 成功数: 2400, 失败数: 240 },
{ name: "二月", 配比率: 72, 成功数: 2210, 失败数: 221 },
{ name: "三月", 配比率: 78, 成功数: 2290, 失败数: 229 },
{ name: "四月", 配比率: 84, 成功数: 2000, 失败数: 200 },
{ name: "五月", 配比率: 90, 成功数: 2181, 失败数: 218 },
{ name: "六月", 配比率: 94, 成功数: 2500, 失败数: 250 },
]
export default function DataRatioChart() {
return (
<div className="lg:col-span-3 space-y-6">
<div className="border-border bg-card/50 backdrop-blur p-6">
<h3 className="text-base font-semibold text-foreground mb-4"></h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="rgb(var(--border))" />
<XAxis dataKey="name" stroke="rgb(var(--muted-foreground))" />
<YAxis stroke="rgb(var(--muted-foreground))" />
<Tooltip
contentStyle={{
backgroundColor: "rgb(var(--card))",
border: "1px solid rgb(var(--border))",
borderRadius: "8px",
}}
cursor={{ fill: "rgba(59, 130, 246, 0.1)" }}
/>
<Legend />
<Bar dataKey="成功数" stackId="a" fill="#3b82f6" radius={[8, 8, 0, 0]} />
<Bar dataKey="失败数" stackId="a" fill="#ef4444" radius={[8, 8, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
<div className="border-border bg-card/50 backdrop-blur p-6">
<h3 className="text-base font-semibold text-foreground mb-4">线</h3>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="rgb(var(--border))" />
<XAxis dataKey="name" stroke="rgb(var(--muted-foreground))" />
<YAxis stroke="rgb(var(--muted-foreground))" />
<Tooltip
contentStyle={{
backgroundColor: "rgb(var(--card))",
border: "1px solid rgb(var(--border))",
borderRadius: "8px",
}}
cursor={{ stroke: "rgba(34, 197, 94, 0.2)" }}
/>
<Line
type="monotone"
dataKey="配比率"
stroke="#22c55e"
strokeWidth={2}
dot={{ fill: "#22c55e", r: 4 }}
activeDot={{ r: 6 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
)
}

View File

@@ -1,107 +1,107 @@
import { BarChart3, Layers } from "lucide-react";
import {
PieChart,
Pie,
Cell,
Tooltip,
ResponsiveContainer,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
} from "recharts";
interface DatasetRatio {
name: string;
ratio: number;
count: number;
color: string;
}
export default function RatioDisplay() {
const datasets: DatasetRatio[] = [
{ name: "用户行为数据集", ratio: 45, count: 450000, color: "#3b82f6" },
{ name: "交易记录数据集", ratio: 30, count: 300000, color: "#8b5cf6" },
{ name: "产品信息数据集", ratio: 15, count: 150000, color: "#ec4899" },
{ name: "评价反馈数据集", ratio: 10, count: 100000, color: "#f59e0b" },
];
const chartData = datasets.map((d) => ({
name: d.name,
value: d.ratio,
count: d.count,
fill: d.color,
}));
return (
<div className="mt-8">
<h3 className="text-lg font-semibold text-foreground mb-6 flex items-center gap-2">
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* 饼图展示比例 */}
<div className="flex items-center justify-center">
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, value }) => `${value}%`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
</Pie>
<Tooltip formatter={(value) => `${value}%`} />
</PieChart>
</ResponsiveContainer>
</div>
{/* 数据集详情列表 */}
<div className="space-y-4">
{datasets.map((dataset, index) => (
<div key={index} className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: dataset.color }}
></div>
<span className="font-medium text-foreground text-sm">
{dataset.name}
</span>
</div>
<span className="text-sm font-semibold text-foreground">
{dataset.ratio}%
</span>
</div>
{/* 比例条形图 */}
<div className="flex items-center gap-3">
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{
width: `${dataset.ratio}%`,
backgroundColor: dataset.color,
}}
></div>
</div>
<span className="text-xs text-muted-foreground min-w-fit">
{dataset.count.toLocaleString()}
</span>
</div>
</div>
))}
</div>
</div>
</div>
);
}
import { BarChart3, Layers } from "lucide-react";
import {
PieChart,
Pie,
Cell,
Tooltip,
ResponsiveContainer,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
} from "recharts";
interface DatasetRatio {
name: string;
ratio: number;
count: number;
color: string;
}
export default function RatioDisplay() {
const datasets: DatasetRatio[] = [
{ name: "用户行为数据集", ratio: 45, count: 450000, color: "#3b82f6" },
{ name: "交易记录数据集", ratio: 30, count: 300000, color: "#8b5cf6" },
{ name: "产品信息数据集", ratio: 15, count: 150000, color: "#ec4899" },
{ name: "评价反馈数据集", ratio: 10, count: 100000, color: "#f59e0b" },
];
const chartData = datasets.map((d) => ({
name: d.name,
value: d.ratio,
count: d.count,
fill: d.color,
}));
return (
<div className="mt-8">
<h3 className="text-lg font-semibold text-foreground mb-6 flex items-center gap-2">
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* 饼图展示比例 */}
<div className="flex items-center justify-center">
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, value }) => `${value}%`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
</Pie>
<Tooltip formatter={(value) => `${value}%`} />
</PieChart>
</ResponsiveContainer>
</div>
{/* 数据集详情列表 */}
<div className="space-y-4">
{datasets.map((dataset, index) => (
<div key={index} className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: dataset.color }}
></div>
<span className="font-medium text-foreground text-sm">
{dataset.name}
</span>
</div>
<span className="text-sm font-semibold text-foreground">
{dataset.ratio}%
</span>
</div>
{/* 比例条形图 */}
<div className="flex items-center gap-3">
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all"
style={{
width: `${dataset.ratio}%`,
backgroundColor: dataset.color,
}}
></div>
</div>
<span className="text-xs text-muted-foreground min-w-fit">
{dataset.count.toLocaleString()}
</span>
</div>
</div>
))}
</div>
</div>
</div>
);
}

View File

@@ -1,253 +1,253 @@
import { useEffect, useMemo, useState, useCallback } from "react";
import {
Breadcrumb,
App,
Tabs,
Button,
Card,
Progress,
Badge,
Descriptions,
DescriptionsProps,
} from "antd";
import { ReloadOutlined, DeleteOutlined } from "@ant-design/icons";
import DetailHeader from "@/components/DetailHeader";
import { Link, useNavigate, useParams } from "react-router";
import {
getRatioTaskByIdUsingGet,
deleteRatioTasksUsingDelete,
} from "@/pages/RatioTask/ratio.api";
import { post } from "@/utils/request";
import type { RatioTaskItem } from "@/pages/RatioTask/ratio.model";
import { mapRatioTask } from "../ratio.const";
import { Copy, Pause, PlayIcon } from "lucide-react";
import DataRatioChart from "./DataRatioChart";
import RatioDisplay from "./RatioDisplay";
import DataMetrics from "./DataMetrics";
const tabList = [
{
key: "overview",
label: "概览",
},
// {
// key: "analysis",
// label: "配比分析",
// },
// {
// key: "config",
// label: "配比配置",
// },
];
export default function RatioTaskDetail() {
const { id } = useParams();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("overview");
const { message } = App.useApp();
const [ratioTask, setRatioTask] = useState<RatioTaskItem>(
{} as RatioTaskItem
);
const navigateItems = useMemo(
() => [
{
title: <Link to="/data/synthesis/ratio-task"></Link>,
},
{
title: ratioTask.name || "配比任务详情",
},
],
[ratioTask]
);
const fetchRatioTask = useCallback(async () => {
const { data } = await getRatioTaskByIdUsingGet(id as string);
setRatioTask(mapRatioTask(data));
}, [id]);
useEffect(() => {
fetchRatioTask();
}, []);
const handleRefresh = useCallback(
async (showMessage = true) => {
await fetchRatioTask();
if (showMessage) message.success({ content: "任务数据刷新成功" });
},
[fetchRatioTask, message]
);
const handleDelete = async () => {
await deleteRatioTasksUsingDelete(id as string);
navigate("/ratio/task");
message.success("配比任务删除成功");
};
const handleExecute = async () => {
await post(`/api/synthesis/ratio-task/${id}/execute`);
handleRefresh();
message.success("任务已启动");
};
const handleStop = async () => {
await post(`/api/synthesis/ratio-task/${id}/stop`);
handleRefresh();
message.success("任务已停止");
};
useEffect(() => {
const refreshData = () => {
handleRefresh(false);
};
window.addEventListener("update:ratio-task", refreshData);
return () => {
window.removeEventListener("update:ratio-task", refreshData);
};
}, [handleRefresh]);
// 操作列表
const operations = [
// {
// key: "execute",
// label: "启动",
// icon: <PlayIcon className="w-4 h-4 text-gray-500" />,
// onClick: handleExecute,
// disabled: ratioTask.status === "RUNNING",
// },
// {
// key: "stop",
// label: "停止",
// icon: <Pause className="w-4 h-4 text-gray-500" />,
// onClick: handleStop,
// disabled: ratioTask.status !== "RUNNING",
// },
{
key: "refresh",
label: "刷新",
icon: <ReloadOutlined />,
onClick: handleRefresh,
},
{
key: "delete",
label: "删除",
danger: true,
confirm: {
title: "确认删除该配比任务?",
description: "删除后该任务将无法恢复,请谨慎操作。",
okText: "删除",
cancelText: "取消",
okType: "danger",
},
icon: <DeleteOutlined />,
onClick: handleDelete,
},
];
// 基本信息
const items: DescriptionsProps["items"] = [
{
key: "id",
label: "ID",
children: ratioTask.id,
},
{
key: "name",
label: "名称",
children: ratioTask.name,
},
{
key: "totals",
label: "目标数量",
children: ratioTask.totals,
},
{
key: "dataset",
label: "目标数据集",
children: (
<Link to={`/data/management/detail/${ratioTask.target_dataset_id}`}>
{ratioTask.target_dataset_name}
</Link>
),
},
{
key: "status",
label: "状态",
children: (
<Badge color={ratioTask.status?.color} text={ratioTask.status?.label} />
),
},
{
key: "createdBy",
label: "创建者",
children: ratioTask.createdBy || "未知",
},
{
key: "createdAt",
label: "创建时间",
children: ratioTask.createdAt,
},
{
key: "updatedAt",
label: "更新时间",
children: ratioTask.updatedAt,
},
{
key: "description",
label: "描述",
children: ratioTask.description || "无",
},
];
return (
<div className="h-full flex flex-col gap-4">
<Breadcrumb items={navigateItems} />
{/* Header */}
<DetailHeader
data={ratioTask}
statistics={ratioTask?.statistics || []}
operations={operations}
/>
{/* <DataMetrics /> */}
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
<div className="h-full overflow-auto">
{activeTab === "overview" && (
<>
<Descriptions
title="基本信息"
layout="vertical"
size="small"
items={items}
column={5}
/>
{/* <RatioDisplay /> */}
</>
)}
{activeTab === "analysis" && <DataRatioChart />}
{activeTab === "config" && (
<div className="bg-gray-50 rounded-lg p-4 font-mono text-xs overflow-x-auto">
<pre className="text-gray-700 whitespace-pre-wrap break-words">
{JSON.stringify(
{
id: ratioTask.id,
name: ratioTask.name,
type: ratioTask.type,
status: ratioTask.status,
strategy: ratioTask.strategy,
sourceDatasets: ratioTask.sourceDatasets,
targetRatio: ratioTask.targetRatio,
outputPath: ratioTask.outputPath,
createdAt: ratioTask.createdAt,
},
null,
2
)}
</pre>
</div>
)}
</div>
</div>
</div>
);
}
import { useEffect, useMemo, useState, useCallback } from "react";
import {
Breadcrumb,
App,
Tabs,
Button,
Card,
Progress,
Badge,
Descriptions,
DescriptionsProps,
} from "antd";
import { ReloadOutlined, DeleteOutlined } from "@ant-design/icons";
import DetailHeader from "@/components/DetailHeader";
import { Link, useNavigate, useParams } from "react-router";
import {
getRatioTaskByIdUsingGet,
deleteRatioTasksUsingDelete,
} from "@/pages/RatioTask/ratio.api";
import { post } from "@/utils/request";
import type { RatioTaskItem } from "@/pages/RatioTask/ratio.model";
import { mapRatioTask } from "../ratio.const";
import { Copy, Pause, PlayIcon } from "lucide-react";
import DataRatioChart from "./DataRatioChart";
import RatioDisplay from "./RatioDisplay";
import DataMetrics from "./DataMetrics";
const tabList = [
{
key: "overview",
label: "概览",
},
// {
// key: "analysis",
// label: "配比分析",
// },
// {
// key: "config",
// label: "配比配置",
// },
];
export default function RatioTaskDetail() {
const { id } = useParams();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("overview");
const { message } = App.useApp();
const [ratioTask, setRatioTask] = useState<RatioTaskItem>(
{} as RatioTaskItem
);
const navigateItems = useMemo(
() => [
{
title: <Link to="/data/synthesis/ratio-task"></Link>,
},
{
title: ratioTask.name || "配比任务详情",
},
],
[ratioTask]
);
const fetchRatioTask = useCallback(async () => {
const { data } = await getRatioTaskByIdUsingGet(id as string);
setRatioTask(mapRatioTask(data));
}, [id]);
useEffect(() => {
fetchRatioTask();
}, []);
const handleRefresh = useCallback(
async (showMessage = true) => {
await fetchRatioTask();
if (showMessage) message.success({ content: "任务数据刷新成功" });
},
[fetchRatioTask, message]
);
const handleDelete = async () => {
await deleteRatioTasksUsingDelete(id as string);
navigate("/ratio/task");
message.success("配比任务删除成功");
};
const handleExecute = async () => {
await post(`/api/synthesis/ratio-task/${id}/execute`);
handleRefresh();
message.success("任务已启动");
};
const handleStop = async () => {
await post(`/api/synthesis/ratio-task/${id}/stop`);
handleRefresh();
message.success("任务已停止");
};
useEffect(() => {
const refreshData = () => {
handleRefresh(false);
};
window.addEventListener("update:ratio-task", refreshData);
return () => {
window.removeEventListener("update:ratio-task", refreshData);
};
}, [handleRefresh]);
// 操作列表
const operations = [
// {
// key: "execute",
// label: "启动",
// icon: <PlayIcon className="w-4 h-4 text-gray-500" />,
// onClick: handleExecute,
// disabled: ratioTask.status === "RUNNING",
// },
// {
// key: "stop",
// label: "停止",
// icon: <Pause className="w-4 h-4 text-gray-500" />,
// onClick: handleStop,
// disabled: ratioTask.status !== "RUNNING",
// },
{
key: "refresh",
label: "刷新",
icon: <ReloadOutlined />,
onClick: handleRefresh,
},
{
key: "delete",
label: "删除",
danger: true,
confirm: {
title: "确认删除该配比任务?",
description: "删除后该任务将无法恢复,请谨慎操作。",
okText: "删除",
cancelText: "取消",
okType: "danger",
},
icon: <DeleteOutlined />,
onClick: handleDelete,
},
];
// 基本信息
const items: DescriptionsProps["items"] = [
{
key: "id",
label: "ID",
children: ratioTask.id,
},
{
key: "name",
label: "名称",
children: ratioTask.name,
},
{
key: "totals",
label: "目标数量",
children: ratioTask.totals,
},
{
key: "dataset",
label: "目标数据集",
children: (
<Link to={`/data/management/detail/${ratioTask.target_dataset_id}`}>
{ratioTask.target_dataset_name}
</Link>
),
},
{
key: "status",
label: "状态",
children: (
<Badge color={ratioTask.status?.color} text={ratioTask.status?.label} />
),
},
{
key: "createdBy",
label: "创建者",
children: ratioTask.createdBy || "未知",
},
{
key: "createdAt",
label: "创建时间",
children: ratioTask.createdAt,
},
{
key: "updatedAt",
label: "更新时间",
children: ratioTask.updatedAt,
},
{
key: "description",
label: "描述",
children: ratioTask.description || "无",
},
];
return (
<div className="h-full flex flex-col gap-4">
<Breadcrumb items={navigateItems} />
{/* Header */}
<DetailHeader
data={ratioTask}
statistics={ratioTask?.statistics || []}
operations={operations}
/>
{/* <DataMetrics /> */}
<div className="flex-overflow-auto p-6 pt-2 bg-white rounded-md shadow">
<Tabs activeKey={activeTab} items={tabList} onChange={setActiveTab} />
<div className="h-full overflow-auto">
{activeTab === "overview" && (
<>
<Descriptions
title="基本信息"
layout="vertical"
size="small"
items={items}
column={5}
/>
{/* <RatioDisplay /> */}
</>
)}
{activeTab === "analysis" && <DataRatioChart />}
{activeTab === "config" && (
<div className="bg-gray-50 rounded-lg p-4 font-mono text-xs overflow-x-auto">
<pre className="text-gray-700 whitespace-pre-wrap break-words">
{JSON.stringify(
{
id: ratioTask.id,
name: ratioTask.name,
type: ratioTask.type,
status: ratioTask.status,
strategy: ratioTask.strategy,
sourceDatasets: ratioTask.sourceDatasets,
targetRatio: ratioTask.targetRatio,
outputPath: ratioTask.outputPath,
createdAt: ratioTask.createdAt,
},
null,
2
)}
</pre>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,227 +1,227 @@
import { useState } from "react";
import { Button, Card, Table, App, Badge, Popconfirm } from "antd";
import { Plus } from "lucide-react";
import { DeleteOutlined } from "@ant-design/icons";
import type { RatioTaskItem } from "@/pages/RatioTask/ratio.model";
import { useNavigate } from "react-router";
import CardView from "@/components/CardView";
import { SearchControls } from "@/components/SearchControls";
import {
deleteRatioTasksUsingDelete,
queryRatioTasksUsingGet,
} from "../ratio.api";
import useFetchData from "@/hooks/useFetchData";
import { mapRatioTask } from "../ratio.const";
export default function RatioTasksPage() {
const { message } = App.useApp();
const navigate = useNavigate();
const [viewMode, setViewMode] = useState<"card" | "list">("list");
const {
loading,
tableData,
pagination,
searchParams,
setSearchParams,
handleFiltersChange,
fetchData,
handleKeywordChange,
} = useFetchData<RatioTaskItem>(
queryRatioTasksUsingGet,
mapRatioTask,
30000,
true,
[],
0
);
const handleDeleteTask = async (task: RatioTaskItem) => {
try {
// 调用删除接口
await deleteRatioTasksUsingDelete(task.id);
message.success("任务删除成功");
// 重新加载数据
fetchData();
} catch (error) {
message.error("任务删除失败,请稍后重试");
}
};
// 搜索、筛选和视图控制相关
const filters = [
{
key: "status",
label: "状态筛选",
options: [
{ label: "全部状态", value: "all" },
{ label: "等待中", value: "PENDING" },
{ label: "运行中", value: "RUNNING" },
{ label: "已完成", value: "SUCCESS" },
{ label: "失败", value: "FAILED" },
{ label: "已暂停", value: "PAUSED" },
],
},
];
const columns = [
{
title: "任务名称",
dataIndex: "name",
key: "name",
width: 200,
fixed: "left" as const,
render: (text: string, record: RatioTaskItem) => (
<a
onClick={() =>
navigate(`/data/synthesis/ratio-task/detail/${record.id}`)
}
>
{text}
</a>
),
},
{
title: "状态",
dataIndex: "status",
key: "status",
width: 120,
render: (status) => {
return (
<Badge
color={status?.color}
icon={status?.icon}
text={status?.label}
/>
);
},
},
{
title: "目标数量",
dataIndex: "totals",
key: "totals",
width: 120,
},
{
title: "目标数据集",
dataIndex: "target_dataset_name",
key: "target_dataset_name",
render: (text: string, task: RatioTaskItem) => (
<a
onClick={() =>
navigate(`/data/management/detail/${task.target_dataset_id}`)
}
>
{text}
</a>
),
},
{
title: "创建时间",
dataIndex: "created_at",
key: "created_at",
width: 180,
},
{
title: "操作",
key: "actions",
width: 120,
fixed: "right" as const,
render: (_: any, task: RatioTaskItem) => (
<div className="flex items-center gap-2">
{operations.map((op) => {
if (op.confirm) {
<Popconfirm
title={op.confirm.title}
description={op.confirm.description}
onConfirm={() => op.onClick(task)}
>
<Button type="text" icon={op.icon} />
</Popconfirm>;
}
return (
<Button
key={op.key}
type="text"
icon={op.icon}
danger={op.danger}
onClick={() => op.onClick(task)}
/>
);
})}
</div>
),
},
];
const operations = [
{
key: "delete",
label: "删除",
danger: true,
confirm: {
title: "确认删除该任务?",
description: "删除后该任务将无法恢复,请谨慎操作。",
okText: "删除",
cancelText: "取消",
okType: "danger",
},
icon: <DeleteOutlined />,
onClick: handleDeleteTask,
},
];
return (
<div className="h-full flex flex-col gap-4">
<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={handleKeywordChange}
searchPlaceholder="搜索任务名称..."
filters={filters}
onFiltersChange={handleFiltersChange}
onClearFilters={() =>
setSearchParams({ ...searchParams, filter: {} })
}
viewMode={viewMode}
onViewModeChange={setViewMode}
showViewToggle
onReload={fetchData}
/>
{/* 任务列表 */}
{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>
);
}
import { useState } from "react";
import { Button, Card, Table, App, Badge, Popconfirm } from "antd";
import { Plus } from "lucide-react";
import { DeleteOutlined } from "@ant-design/icons";
import type { RatioTaskItem } from "@/pages/RatioTask/ratio.model";
import { useNavigate } from "react-router";
import CardView from "@/components/CardView";
import { SearchControls } from "@/components/SearchControls";
import {
deleteRatioTasksUsingDelete,
queryRatioTasksUsingGet,
} from "../ratio.api";
import useFetchData from "@/hooks/useFetchData";
import { mapRatioTask } from "../ratio.const";
export default function RatioTasksPage() {
const { message } = App.useApp();
const navigate = useNavigate();
const [viewMode, setViewMode] = useState<"card" | "list">("list");
const {
loading,
tableData,
pagination,
searchParams,
setSearchParams,
handleFiltersChange,
fetchData,
handleKeywordChange,
} = useFetchData<RatioTaskItem>(
queryRatioTasksUsingGet,
mapRatioTask,
30000,
true,
[],
0
);
const handleDeleteTask = async (task: RatioTaskItem) => {
try {
// 调用删除接口
await deleteRatioTasksUsingDelete(task.id);
message.success("任务删除成功");
// 重新加载数据
fetchData();
} catch (error) {
message.error("任务删除失败,请稍后重试");
}
};
// 搜索、筛选和视图控制相关
const filters = [
{
key: "status",
label: "状态筛选",
options: [
{ label: "全部状态", value: "all" },
{ label: "等待中", value: "PENDING" },
{ label: "运行中", value: "RUNNING" },
{ label: "已完成", value: "SUCCESS" },
{ label: "失败", value: "FAILED" },
{ label: "已暂停", value: "PAUSED" },
],
},
];
const columns = [
{
title: "任务名称",
dataIndex: "name",
key: "name",
width: 200,
fixed: "left" as const,
render: (text: string, record: RatioTaskItem) => (
<a
onClick={() =>
navigate(`/data/synthesis/ratio-task/detail/${record.id}`)
}
>
{text}
</a>
),
},
{
title: "状态",
dataIndex: "status",
key: "status",
width: 120,
render: (status) => {
return (
<Badge
color={status?.color}
icon={status?.icon}
text={status?.label}
/>
);
},
},
{
title: "目标数量",
dataIndex: "totals",
key: "totals",
width: 120,
},
{
title: "目标数据集",
dataIndex: "target_dataset_name",
key: "target_dataset_name",
render: (text: string, task: RatioTaskItem) => (
<a
onClick={() =>
navigate(`/data/management/detail/${task.target_dataset_id}`)
}
>
{text}
</a>
),
},
{
title: "创建时间",
dataIndex: "created_at",
key: "created_at",
width: 180,
},
{
title: "操作",
key: "actions",
width: 120,
fixed: "right" as const,
render: (_: any, task: RatioTaskItem) => (
<div className="flex items-center gap-2">
{operations.map((op) => {
if (op.confirm) {
<Popconfirm
title={op.confirm.title}
description={op.confirm.description}
onConfirm={() => op.onClick(task)}
>
<Button type="text" icon={op.icon} />
</Popconfirm>;
}
return (
<Button
key={op.key}
type="text"
icon={op.icon}
danger={op.danger}
onClick={() => op.onClick(task)}
/>
);
})}
</div>
),
},
];
const operations = [
{
key: "delete",
label: "删除",
danger: true,
confirm: {
title: "确认删除该任务?",
description: "删除后该任务将无法恢复,请谨慎操作。",
okText: "删除",
cancelText: "取消",
okType: "danger",
},
icon: <DeleteOutlined />,
onClick: handleDeleteTask,
},
];
return (
<div className="h-full flex flex-col gap-4">
<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={handleKeywordChange}
searchPlaceholder="搜索任务名称..."
filters={filters}
onFiltersChange={handleFiltersChange}
onClearFilters={() =>
setSearchParams({ ...searchParams, filter: {} })
}
viewMode={viewMode}
onViewModeChange={setViewMode}
showViewToggle
onReload={fetchData}
/>
{/* 任务列表 */}
{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>
);
}

View File

@@ -1,22 +1,22 @@
import { get, post, put, del, download } from "@/utils/request";
// 查询配比任务列表(分页)
export function queryRatioTasksUsingGet(params?: any) {
return get("/api/synthesis/ratio-task", params);
}
// 查询配比任务详情
export function getRatioTaskByIdUsingGet(id: string) {
return get(`/api/synthesis/ratio-task/${id}`);
}
// 创建配比任务
export function createRatioTaskUsingPost(data: any) {
return post("/api/synthesis/ratio-task", data);
}
// 删除配比任务(支持批量)
export function deleteRatioTasksUsingDelete(id: string) {
const url = `/api/synthesis/ratio-task?ids=${id}`;
return del(url);
}
import { get, post, put, del, download } from "@/utils/request";
// 查询配比任务列表(分页)
export function queryRatioTasksUsingGet(params?: any) {
return get("/api/synthesis/ratio-task", params);
}
// 查询配比任务详情
export function getRatioTaskByIdUsingGet(id: string) {
return get(`/api/synthesis/ratio-task/${id}`);
}
// 创建配比任务
export function createRatioTaskUsingPost(data: any) {
return post("/api/synthesis/ratio-task", data);
}
// 删除配比任务(支持批量)
export function deleteRatioTasksUsingDelete(id: string) {
const url = `/api/synthesis/ratio-task?ids=${id}`;
return del(url);
}

View File

@@ -1,75 +1,75 @@
import { formatDateTime } from "@/utils/unit";
import { RatioTaskItem, RatioStatus } from "./ratio.model";
import { BarChart3, Calendar, Database } from "lucide-react";
import { Link } from "react-router";
export const ratioTaskStatusMap: Record<
string,
{
value: RatioStatus;
label: string;
color: string;
icon?: React.ReactNode;
}
> = {
[RatioStatus.PENDING]: {
value: RatioStatus.PENDING,
label: "等待中",
color: "gray",
},
[RatioStatus.RUNNING]: {
value: RatioStatus.RUNNING,
label: "运行中",
color: "blue",
},
[RatioStatus.COMPLETED]: {
value: RatioStatus.COMPLETED,
label: "已完成",
color: "green",
},
[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],
createdAt: formatDateTime(task.created_at),
updatedAt: formatDateTime(task.updated_at),
description: task.description,
icon: <BarChart3 />,
iconColor: task.ratio_method === "DATASET" ? "bg-blue-100" : "bg-green-100",
statistics: [
{
label: "目标数量",
icon: <BarChart3 className="w-4 h-4 text-gray-500" />,
value: (task.totals ?? 0).toLocaleString(),
},
{
label: "目标数据集",
icon: <Database className="w-4 h-4 text-gray-500" />,
value: task.target_dataset_name ? (
<Link to={`/data/management/detail/${task.target_dataset_id}`}>
{task.target_dataset_name}
</Link>
) : (
"无"
),
},
{
label: "创建时间",
icon: <Calendar className="w-4 h-4 text-gray-500" />,
value: task.created_at || "-",
},
],
};
}
import { formatDateTime } from "@/utils/unit";
import { RatioTaskItem, RatioStatus } from "./ratio.model";
import { BarChart3, Calendar, Database } from "lucide-react";
import { Link } from "react-router";
export const ratioTaskStatusMap: Record<
string,
{
value: RatioStatus;
label: string;
color: string;
icon?: React.ReactNode;
}
> = {
[RatioStatus.PENDING]: {
value: RatioStatus.PENDING,
label: "等待中",
color: "gray",
},
[RatioStatus.RUNNING]: {
value: RatioStatus.RUNNING,
label: "运行中",
color: "blue",
},
[RatioStatus.COMPLETED]: {
value: RatioStatus.COMPLETED,
label: "已完成",
color: "green",
},
[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],
createdAt: formatDateTime(task.created_at),
updatedAt: formatDateTime(task.updated_at),
description: task.description,
icon: <BarChart3 />,
iconColor: task.ratio_method === "DATASET" ? "bg-blue-100" : "bg-green-100",
statistics: [
{
label: "目标数量",
icon: <BarChart3 className="w-4 h-4 text-gray-500" />,
value: (task.totals ?? 0).toLocaleString(),
},
{
label: "目标数据集",
icon: <Database className="w-4 h-4 text-gray-500" />,
value: task.target_dataset_name ? (
<Link to={`/data/management/detail/${task.target_dataset_id}`}>
{task.target_dataset_name}
</Link>
) : (
"无"
),
},
{
label: "创建时间",
icon: <Calendar className="w-4 h-4 text-gray-500" />,
value: task.created_at || "-",
},
],
};
}

View File

@@ -1,92 +1,92 @@
// Ratio module models aligned with scripts/db/data-ratio-init.sql
// enums
export type RatioMethod = "TAG" | "DATASET";
export enum RatioStatus {
PENDING = "PENDING",
RUNNING = "RUNNING",
COMPLETED = "COMPLETED",
FAILED = "FAILED",
PAUSED = "PAUSED",
}
// interfaces
// t_st_ratio_instances
export interface RatioInstance {
id: string;
name: string;
description?: string;
targetDatasetId?: string;
ratioMethod?: RatioMethod;
ratioParameters?: any;
mergeMethod?: string;
status?: RatioStatus | string;
totals?: number;
createdAt?: string;
updatedAt?: string;
createdBy?: string;
updatedBy?: string;
}
// 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
config: RatioConfigItem[]
created_at?: string
updated_at?: string
}
// Ratio module models aligned with scripts/db/data-ratio-init.sql
// enums
export type RatioMethod = "TAG" | "DATASET";
export enum RatioStatus {
PENDING = "PENDING",
RUNNING = "RUNNING",
COMPLETED = "COMPLETED",
FAILED = "FAILED",
PAUSED = "PAUSED",
}
// interfaces
// t_st_ratio_instances
export interface RatioInstance {
id: string;
name: string;
description?: string;
targetDatasetId?: string;
ratioMethod?: RatioMethod;
ratioParameters?: any;
mergeMethod?: string;
status?: RatioStatus | string;
totals?: number;
createdAt?: string;
updatedAt?: string;
createdBy?: string;
updatedBy?: string;
}
// 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
config: RatioConfigItem[]
created_at?: string
updated_at?: string
}