You've already forked DataMate
feat: fix the problem in the Operator Market frontend pages
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 || "-",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user