feat: fix the problem in the Operator Market frontend pages

This commit is contained in:
root
2025-12-29 11:38:47 +08:00
parent 29e4a333a9
commit 844add27ea
213 changed files with 45547 additions and 45537 deletions

View File

@@ -1,133 +1,133 @@
import { useState } from "react";
import { Steps, Button, message, Form } from "antd";
import { SaveOutlined } from "@ant-design/icons";
import { Link, useNavigate } from "react-router";
import { ArrowLeft } from "lucide-react";
import { createCleaningTaskUsingPost } from "../cleansing.api";
import CreateTaskStepOne from "./components/CreateTaskStepOne";
import { useCreateStepTwo } from "./hooks/useCreateStepTwo";
import { DatasetType } from "@/pages/DataManagement/dataset.model";
export default function CleansingTaskCreate() {
const navigate = useNavigate();
const [form] = Form.useForm();
const [taskConfig, setTaskConfig] = useState({
name: "",
description: "",
srcDatasetId: "",
srcDatasetName: "",
destDatasetName: "",
destDatasetType: DatasetType.TEXT,
type: DatasetType.TEXT,
});
const {
renderStepTwo,
selectedOperators,
currentStep,
handlePrev,
handleNext,
} = useCreateStepTwo();
const handleSave = async () => {
const task = {
...taskConfig,
instance: selectedOperators.map((item) => ({
id: item.id,
overrides: {
...item.defaultParams,
...item.overrides,
},
inputs: item.inputs,
outputs: item.outputs,
})),
};
navigate("/data/cleansing?view=task");
await createCleaningTaskUsingPost(task);
message.success("任务已创建");
};
const canProceed = () => {
switch (currentStep) {
case 1: {
const values = form.getFieldsValue();
return (
values.name &&
values.srcDatasetId &&
values.destDatasetName &&
values.destDatasetType
);
}
case 2:
return selectedOperators.length > 0;
default:
return false;
}
};
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<CreateTaskStepOne
form={form}
taskConfig={taskConfig}
setTaskConfig={setTaskConfig}
/>
);
case 2:
return renderStepTwo;
default:
return null;
}
};
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<Link to="/data/cleansing">
<Button type="text">
<ArrowLeft className="w-4 h-4 mr-1" />
</Button>
</Link>
<h1 className="text-xl font-bold"></h1>
</div>
<div className="w-1/2">
<Steps
size="small"
current={currentStep - 1}
items={[{ title: "基本信息" }, { title: "算子编排" }]}
/>
</div>
</div>
{/* Step Content */}
<div className="flex-overflow-auto bg-white border-card">
<div className="flex-1 overflow-auto m-6">{renderStepContent()}</div>
<div className="flex justify-end p-6 gap-3 border-top">
<Button onClick={() => navigate("/data/cleansing")}></Button>
{currentStep > 1 && <Button onClick={handlePrev}></Button>}
{currentStep === 2 ? (
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSave}
disabled={!canProceed()}
>
</Button>
) : (
<Button
type="primary"
onClick={handleNext}
disabled={!canProceed()}
>
</Button>
)}
</div>
</div>
</div>
);
}
import { useState } from "react";
import { Steps, Button, message, Form } from "antd";
import { SaveOutlined } from "@ant-design/icons";
import { Link, useNavigate } from "react-router";
import { ArrowLeft } from "lucide-react";
import { createCleaningTaskUsingPost } from "../cleansing.api";
import CreateTaskStepOne from "./components/CreateTaskStepOne";
import { useCreateStepTwo } from "./hooks/useCreateStepTwo";
import { DatasetType } from "@/pages/DataManagement/dataset.model";
export default function CleansingTaskCreate() {
const navigate = useNavigate();
const [form] = Form.useForm();
const [taskConfig, setTaskConfig] = useState({
name: "",
description: "",
srcDatasetId: "",
srcDatasetName: "",
destDatasetName: "",
destDatasetType: DatasetType.TEXT,
type: DatasetType.TEXT,
});
const {
renderStepTwo,
selectedOperators,
currentStep,
handlePrev,
handleNext,
} = useCreateStepTwo();
const handleSave = async () => {
const task = {
...taskConfig,
instance: selectedOperators.map((item) => ({
id: item.id,
overrides: {
...item.defaultParams,
...item.overrides,
},
inputs: item.inputs,
outputs: item.outputs,
})),
};
navigate("/data/cleansing?view=task");
await createCleaningTaskUsingPost(task);
message.success("任务已创建");
};
const canProceed = () => {
switch (currentStep) {
case 1: {
const values = form.getFieldsValue();
return (
values.name &&
values.srcDatasetId &&
values.destDatasetName &&
values.destDatasetType
);
}
case 2:
return selectedOperators.length > 0;
default:
return false;
}
};
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<CreateTaskStepOne
form={form}
taskConfig={taskConfig}
setTaskConfig={setTaskConfig}
/>
);
case 2:
return renderStepTwo;
default:
return null;
}
};
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<Link to="/data/cleansing">
<Button type="text">
<ArrowLeft className="w-4 h-4 mr-1" />
</Button>
</Link>
<h1 className="text-xl font-bold"></h1>
</div>
<div className="w-1/2">
<Steps
size="small"
current={currentStep - 1}
items={[{ title: "基本信息" }, { title: "算子编排" }]}
/>
</div>
</div>
{/* Step Content */}
<div className="flex-overflow-auto bg-white border-card">
<div className="flex-1 overflow-auto m-6">{renderStepContent()}</div>
<div className="flex justify-end p-6 gap-3 border-top">
<Button onClick={() => navigate("/data/cleansing")}></Button>
{currentStep > 1 && <Button onClick={handlePrev}></Button>}
{currentStep === 2 ? (
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSave}
disabled={!canProceed()}
>
</Button>
) : (
<Button
type="primary"
onClick={handleNext}
disabled={!canProceed()}
>
</Button>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,142 +1,142 @@
import {useEffect, useState} from "react";
import {Button, Steps, Form, message} from "antd";
import {Link, useNavigate, useParams} from "react-router";
import { ArrowLeft } from "lucide-react";
import {
createCleaningTemplateUsingPost,
queryCleaningTemplateByIdUsingGet,
updateCleaningTemplateByIdUsingPut
} from "../cleansing.api";
import CleansingTemplateStepOne from "./components/CreateTemplateStepOne";
import { useCreateStepTwo } from "./hooks/useCreateStepTwo";
export default function CleansingTemplateCreate() {
const { id = "" } = useParams()
const navigate = useNavigate();
const [form] = Form.useForm();
const [templateConfig, setTemplateConfig] = useState({
name: "",
description: "",
});
const fetchTemplateDetail = async () => {
if (!id) return;
try {
const { data } = await queryCleaningTemplateByIdUsingGet(id);
setTemplateConfig(data);
} catch (error) {
message.error("获取任务详情失败");
navigate("/data/cleansing");
}
};
useEffect(() => {
fetchTemplateDetail()
}, [id]);
const handleSave = async () => {
const template = {
...templateConfig,
instance: selectedOperators.map((item) => ({
id: item.id,
overrides: {
...item.defaultParams,
...item.overrides,
},
inputs: item.inputs,
outputs: item.outputs,
})),
};
!id && await createCleaningTemplateUsingPost(template) && message.success("模板创建成功");
id && await updateCleaningTemplateByIdUsingPut(id, template) && message.success("模板更新成功");
navigate("/data/cleansing?view=template");
};
const {
renderStepTwo,
selectedOperators,
currentStep,
handlePrev,
handleNext,
} = useCreateStepTwo();
const canProceed = () => {
const values = form.getFieldsValue();
switch (currentStep) {
case 1:
return values.name;
case 2:
return selectedOperators.length > 0;
default:
return false;
}
};
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<CleansingTemplateStepOne
form={form}
templateConfig={templateConfig}
setTemplateConfig={setTemplateConfig}
/>
);
case 2:
return renderStepTwo;
default:
return null;
}
};
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<Link to="/data/cleansing">
<Button type="text">
<ArrowLeft className="w-4 h-4 mr-1" />
</Button>
</Link>
<h1 className="text-xl font-bold">{id ? '更新清洗模板' : '创建清洗模板'}</h1>
</div>
<div className="w-1/2">
<Steps
size="small"
current={currentStep}
items={[{ title: "基本信息" }, { title: "算子编排" }]}
/>
</div>
</div>
<div className="flex-overflow-auto border-card">
<div className="flex-1 overflow-auto m-6">{renderStepContent()}</div>
<div className="flex justify-end p-6 gap-3 border-top">
<Button onClick={() => navigate("/data/cleansing")}></Button>
{currentStep > 1 && <Button onClick={handlePrev}></Button>}
{currentStep === 2 ? (
<Button
type="primary"
onClick={handleSave}
disabled={!canProceed()}
>
{id ? '更新模板' : '创建模板'}
</Button>
) : (
<Button
type="primary"
onClick={handleNext}
disabled={!canProceed()}
>
</Button>
)}
</div>
</div>
</div>
);
}
import {useEffect, useState} from "react";
import {Button, Steps, Form, message} from "antd";
import {Link, useNavigate, useParams} from "react-router";
import { ArrowLeft } from "lucide-react";
import {
createCleaningTemplateUsingPost,
queryCleaningTemplateByIdUsingGet,
updateCleaningTemplateByIdUsingPut
} from "../cleansing.api";
import CleansingTemplateStepOne from "./components/CreateTemplateStepOne";
import { useCreateStepTwo } from "./hooks/useCreateStepTwo";
export default function CleansingTemplateCreate() {
const { id = "" } = useParams()
const navigate = useNavigate();
const [form] = Form.useForm();
const [templateConfig, setTemplateConfig] = useState({
name: "",
description: "",
});
const fetchTemplateDetail = async () => {
if (!id) return;
try {
const { data } = await queryCleaningTemplateByIdUsingGet(id);
setTemplateConfig(data);
} catch (error) {
message.error("获取任务详情失败");
navigate("/data/cleansing");
}
};
useEffect(() => {
fetchTemplateDetail()
}, [id]);
const handleSave = async () => {
const template = {
...templateConfig,
instance: selectedOperators.map((item) => ({
id: item.id,
overrides: {
...item.defaultParams,
...item.overrides,
},
inputs: item.inputs,
outputs: item.outputs,
})),
};
!id && await createCleaningTemplateUsingPost(template) && message.success("模板创建成功");
id && await updateCleaningTemplateByIdUsingPut(id, template) && message.success("模板更新成功");
navigate("/data/cleansing?view=template");
};
const {
renderStepTwo,
selectedOperators,
currentStep,
handlePrev,
handleNext,
} = useCreateStepTwo();
const canProceed = () => {
const values = form.getFieldsValue();
switch (currentStep) {
case 1:
return values.name;
case 2:
return selectedOperators.length > 0;
default:
return false;
}
};
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<CleansingTemplateStepOne
form={form}
templateConfig={templateConfig}
setTemplateConfig={setTemplateConfig}
/>
);
case 2:
return renderStepTwo;
default:
return null;
}
};
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<Link to="/data/cleansing">
<Button type="text">
<ArrowLeft className="w-4 h-4 mr-1" />
</Button>
</Link>
<h1 className="text-xl font-bold">{id ? '更新清洗模板' : '创建清洗模板'}</h1>
</div>
<div className="w-1/2">
<Steps
size="small"
current={currentStep}
items={[{ title: "基本信息" }, { title: "算子编排" }]}
/>
</div>
</div>
<div className="flex-overflow-auto border-card">
<div className="flex-1 overflow-auto m-6">{renderStepContent()}</div>
<div className="flex justify-end p-6 gap-3 border-top">
<Button onClick={() => navigate("/data/cleansing")}></Button>
{currentStep > 1 && <Button onClick={handlePrev}></Button>}
{currentStep === 2 ? (
<Button
type="primary"
onClick={handleSave}
disabled={!canProceed()}
>
{id ? '更新模板' : '创建模板'}
</Button>
) : (
<Button
type="primary"
onClick={handleNext}
disabled={!canProceed()}
>
</Button>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,113 +1,113 @@
import RadioCard from "@/components/RadioCard";
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
import { datasetTypes, mapDataset } from "@/pages/DataManagement/dataset.const";
import {
Dataset,
DatasetSubType,
DatasetType,
} from "@/pages/DataManagement/dataset.model";
import { Input, Select, Form } from "antd";
import TextArea from "antd/es/input/TextArea";
import { useEffect, useState } from "react";
export default function CreateTaskStepOne({
form,
taskConfig,
setTaskConfig,
}: {
form: any;
taskConfig: {
name: string;
description: string;
datasetId: string;
destDatasetName: string;
type: DatasetType;
destDatasetType: DatasetSubType;
};
setTaskConfig: (config: any) => void;
}) {
const [datasets, setDatasets] = useState<Dataset[]>([]);
const fetchDatasets = async () => {
const { data } = await queryDatasetsUsingGet({ page: 1, size: 1000 });
setDatasets(data.content.map(mapDataset) || []);
};
useEffect(() => {
fetchDatasets();
}, []);
const handleValuesChange = (currentValue, allValues) => {
const [key, value] = Object.entries(currentValue)[0];
let dataset = null;
if (key === "srcDatasetId") {
dataset = datasets.find((d) => d.id === value);
setTaskConfig({
...taskConfig,
...allValues,
srcDatasetName: dataset?.name || "",
});
} else {
setTaskConfig({ ...taskConfig, ...allValues });
}
};
return (
<Form
layout="vertical"
form={form}
initialValues={taskConfig}
onValuesChange={handleValuesChange}
>
<h2 className="font-medium text-gray-900 text-base mb-2"></h2>
<Form.Item label="名称" name="name" required>
<Input placeholder="输入清洗任务名称" />
</Form.Item>
<Form.Item label="描述" name="description">
<TextArea placeholder="描述清洗任务的目标和要求" rows={4} />
</Form.Item>
<h2 className="font-medium text-gray-900 pt-6 mb-2 text-base">
</h2>
<Form.Item label="源数据集" name="srcDatasetId" required>
<Select
placeholder="请选择源数据集"
options={datasets.map((dataset) => {
return {
label: (
<div className="flex items-center justify-between gap-3 py-2">
<div className="flex items-center font-sm text-gray-900">
<span className="mr-2">{dataset.icon}</span>
<span>{dataset.name}</span>
</div>
<div className="text-xs text-gray-500">{dataset.size}</div>
</div>
),
value: dataset.id,
};
})}
/>
</Form.Item>
<Form.Item label="目标数据集名称" name="destDatasetName" required>
<Input placeholder="输入目标数据集名称" />
</Form.Item>
<Form.Item
label="目标数据集类型"
name="destDatasetType"
rules={[{ required: true, message: "请选择目标数据集类型" }]}
>
<RadioCard
options={datasetTypes}
value={taskConfig.destDatasetType}
onChange={(type) => {
form.setFieldValue("destDatasetType", type);
setTaskConfig({
...taskConfig,
destDatasetType: type as DatasetSubType,
});
}}
/>
</Form.Item>
</Form>
);
}
import RadioCard from "@/components/RadioCard";
import { queryDatasetsUsingGet } from "@/pages/DataManagement/dataset.api";
import { datasetTypes, mapDataset } from "@/pages/DataManagement/dataset.const";
import {
Dataset,
DatasetSubType,
DatasetType,
} from "@/pages/DataManagement/dataset.model";
import { Input, Select, Form } from "antd";
import TextArea from "antd/es/input/TextArea";
import { useEffect, useState } from "react";
export default function CreateTaskStepOne({
form,
taskConfig,
setTaskConfig,
}: {
form: any;
taskConfig: {
name: string;
description: string;
datasetId: string;
destDatasetName: string;
type: DatasetType;
destDatasetType: DatasetSubType;
};
setTaskConfig: (config: any) => void;
}) {
const [datasets, setDatasets] = useState<Dataset[]>([]);
const fetchDatasets = async () => {
const { data } = await queryDatasetsUsingGet({ page: 1, size: 1000 });
setDatasets(data.content.map(mapDataset) || []);
};
useEffect(() => {
fetchDatasets();
}, []);
const handleValuesChange = (currentValue, allValues) => {
const [key, value] = Object.entries(currentValue)[0];
let dataset = null;
if (key === "srcDatasetId") {
dataset = datasets.find((d) => d.id === value);
setTaskConfig({
...taskConfig,
...allValues,
srcDatasetName: dataset?.name || "",
});
} else {
setTaskConfig({ ...taskConfig, ...allValues });
}
};
return (
<Form
layout="vertical"
form={form}
initialValues={taskConfig}
onValuesChange={handleValuesChange}
>
<h2 className="font-medium text-gray-900 text-base mb-2"></h2>
<Form.Item label="名称" name="name" required>
<Input placeholder="输入清洗任务名称" />
</Form.Item>
<Form.Item label="描述" name="description">
<TextArea placeholder="描述清洗任务的目标和要求" rows={4} />
</Form.Item>
<h2 className="font-medium text-gray-900 pt-6 mb-2 text-base">
</h2>
<Form.Item label="源数据集" name="srcDatasetId" required>
<Select
placeholder="请选择源数据集"
options={datasets.map((dataset) => {
return {
label: (
<div className="flex items-center justify-between gap-3 py-2">
<div className="flex items-center font-sm text-gray-900">
<span className="mr-2">{dataset.icon}</span>
<span>{dataset.name}</span>
</div>
<div className="text-xs text-gray-500">{dataset.size}</div>
</div>
),
value: dataset.id,
};
})}
/>
</Form.Item>
<Form.Item label="目标数据集名称" name="destDatasetName" required>
<Input placeholder="输入目标数据集名称" />
</Form.Item>
<Form.Item
label="目标数据集类型"
name="destDatasetType"
rules={[{ required: true, message: "请选择目标数据集类型" }]}
>
<RadioCard
options={datasetTypes}
value={taskConfig.destDatasetType}
onChange={(type) => {
form.setFieldValue("destDatasetType", type);
setTaskConfig({
...taskConfig,
destDatasetType: type as DatasetSubType,
});
}}
/>
</Form.Item>
</Form>
);
}

View File

@@ -1,44 +1,44 @@
import { Input, Form } from "antd";
import {useEffect} from "react";
const { TextArea } = Input;
export default function CreateTemplateStepOne({
form,
templateConfig,
setTemplateConfig,
}: {
form: any;
templateConfig: { name: string; description: string; type: string };
setTemplateConfig: React.Dispatch<
React.SetStateAction<{ name: string; description: string; type: string }>
>;
}) {
const handleValuesChange = (_, allValues) => {
setTemplateConfig({ ...templateConfig, ...allValues });
};
useEffect(() => {
form.setFieldsValue(templateConfig);
}, [templateConfig]);
return (
<Form
form={form}
layout="vertical"
initialValues={templateConfig}
onValuesChange={handleValuesChange}
>
<Form.Item
label="模板名称"
name="name"
rules={[{ required: true, message: "请输入模板名称" }]}
>
<Input placeholder="输入模板名称" />
</Form.Item>
<Form.Item label="模板描述" name="description">
<TextArea placeholder="描述模板的用途和特点" rows={4} />
</Form.Item>
</Form>
);
}
import { Input, Form } from "antd";
import {useEffect} from "react";
const { TextArea } = Input;
export default function CreateTemplateStepOne({
form,
templateConfig,
setTemplateConfig,
}: {
form: any;
templateConfig: { name: string; description: string; type: string };
setTemplateConfig: React.Dispatch<
React.SetStateAction<{ name: string; description: string; type: string }>
>;
}) {
const handleValuesChange = (_, allValues) => {
setTemplateConfig({ ...templateConfig, ...allValues });
};
useEffect(() => {
form.setFieldsValue(templateConfig);
}, [templateConfig]);
return (
<Form
form={form}
layout="vertical"
initialValues={templateConfig}
onValuesChange={handleValuesChange}
>
<Form.Item
label="模板名称"
name="name"
rules={[{ required: true, message: "请输入模板名称" }]}
>
<Input placeholder="输入模板名称" />
</Form.Item>
<Form.Item label="模板描述" name="description">
<TextArea placeholder="描述模板的用途和特点" rows={4} />
</Form.Item>
</Form>
);
}

View File

@@ -1,81 +1,81 @@
import React from "react";
import { Tag, Divider, Form } from "antd";
import ParamConfig from "./ParamConfig";
import { Settings } from "lucide-react";
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
// OperatorConfig/OperatorTemplate 类型需根据主文件实际导入
interface OperatorConfigProps {
selectedOp: OperatorI;
renderParamConfig?: (
operator: OperatorI,
paramKey: string,
param: any
) => React.ReactNode;
handleConfigChange?: (
operatorId: string,
paramKey: string,
value: any
) => void;
}
const OperatorConfig: React.FC<OperatorConfigProps> = ({
selectedOp,
renderParamConfig,
handleConfigChange,
}) => {
return (
<div className="w-1/4 min-w-3xs flex flex-col h-full">
<div className="px-4 pb-4 border-b border-gray-200">
<span className="font-semibold text-base flex items-center gap-2">
<Settings />
</span>
</div>
<div className="flex-1 overflow-auto p-4">
{selectedOp ? (
<div>
<div className="mb-4">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium">{selectedOp.name}</span>
</div>
<div className="text-sm text-gray-500">
{selectedOp.description}
</div>
<div className="flex flex-wrap gap-1 mt-2">
{selectedOp?.tags?.map((tag: string) => (
<Tag key={tag} color="default">
{tag}
</Tag>
))}
</div>
</div>
<Divider />
<Form layout="vertical">
{Object.entries(selectedOp.configs).map(([key, param]) =>
renderParamConfig ? (
renderParamConfig(selectedOp, key, param)
) : (
<ParamConfig
key={key}
operator={selectedOp}
paramKey={key}
param={param}
onParamChange={handleConfigChange}
/>
)
)}
</Form>
</div>
) : (
<div className="text-center py-12 text-gray-400">
<Settings className="w-full w-10 h-10 mb-4 opacity-50" />
<div></div>
</div>
)}
</div>
</div>
);
};
export default OperatorConfig;
import React from "react";
import { Tag, Divider, Form } from "antd";
import ParamConfig from "./ParamConfig";
import { Settings } from "lucide-react";
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
// OperatorConfig/OperatorTemplate 类型需根据主文件实际导入
interface OperatorConfigProps {
selectedOp: OperatorI;
renderParamConfig?: (
operator: OperatorI,
paramKey: string,
param: any
) => React.ReactNode;
handleConfigChange?: (
operatorId: string,
paramKey: string,
value: any
) => void;
}
const OperatorConfig: React.FC<OperatorConfigProps> = ({
selectedOp,
renderParamConfig,
handleConfigChange,
}) => {
return (
<div className="w-1/4 min-w-3xs flex flex-col h-full">
<div className="px-4 pb-4 border-b border-gray-200">
<span className="font-semibold text-base flex items-center gap-2">
<Settings />
</span>
</div>
<div className="flex-1 overflow-auto p-4">
{selectedOp ? (
<div>
<div className="mb-4">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium">{selectedOp.name}</span>
</div>
<div className="text-sm text-gray-500">
{selectedOp.description}
</div>
<div className="flex flex-wrap gap-1 mt-2">
{selectedOp?.tags?.map((tag: string) => (
<Tag key={tag} color="default">
{tag}
</Tag>
))}
</div>
</div>
<Divider />
<Form layout="vertical">
{Object.entries(selectedOp.configs).map(([key, param]) =>
renderParamConfig ? (
renderParamConfig(selectedOp, key, param)
) : (
<ParamConfig
key={key}
operator={selectedOp}
paramKey={key}
param={param}
onParamChange={handleConfigChange}
/>
)
)}
</Form>
</div>
) : (
<div className="text-center py-12 text-gray-400">
<Settings className="w-full w-10 h-10 mb-4 opacity-50" />
<div></div>
</div>
)}
</div>
</div>
);
};
export default OperatorConfig;

View File

@@ -1,287 +1,287 @@
import React, {useEffect, useMemo, useState} from "react";
import {Button, Card, Checkbox, Collapse, Input, Select, Tag, Tooltip,} from "antd";
import {SearchOutlined, StarFilled, StarOutlined} from "@ant-design/icons";
import {CategoryI, OperatorI} from "@/pages/OperatorMarket/operator.model";
import {Layers} from "lucide-react";
import {updateOperatorByIdUsingPut} from "@/pages/OperatorMarket/operator.api.ts";
interface OperatorListProps {
operators: OperatorI[];
favorites: Set<string>;
showPoppular?: boolean;
toggleFavorite: (id: string) => void;
toggleOperator: (operator: OperatorI) => void;
selectedOperators: OperatorI[];
onDragOperator: (
e: React.DragEvent,
item: OperatorI,
source: "library"
) => void;
}
const handleStar = async (operator: OperatorI, toggleFavorite: (id: string) => void) => {
const data = {
id: operator.id,
isStar: !operator.isStar
};
await updateOperatorByIdUsingPut(operator.id, data);
toggleFavorite(operator.id)
}
const OperatorList: React.FC<OperatorListProps> = ({
operators,
favorites,
toggleFavorite,
toggleOperator,
selectedOperators,
onDragOperator,
}) => (
<div className="grid grid-cols-1 gap-2">
{operators.map((operator) => {
// 判断是否已选
const isSelected = selectedOperators.some((op) => op.id === operator.id);
return (
<Card
size="small"
key={operator.id}
draggable
hoverable
onDragStart={(e) => onDragOperator(e, operator, "library")}
onClick={() => toggleOperator(operator)}
>
<div className="flex items-center justify-between">
<div className="flex flex-1 min-w-0 items-center gap-2">
<Checkbox checked={isSelected} />
<span className="flex-1 min-w-0 font-medium text-sm overflow-hidden text-ellipsis whitespace-nowrap">
{operator.name}
</span>
</div>
<span
className="cursor-pointer"
onClick={(event) => {
event.stopPropagation();
handleStar(operator, toggleFavorite);
}}
>
{favorites.has(operator.id) ? (
<StarFilled style={{ color: "#FFD700" }} />
) : (
<StarOutlined />
)}
</span>
</div>
</Card>
);
})}
</div>
);
interface OperatorLibraryProps {
selectedOperators: OperatorI[];
operatorList: OperatorI[];
categoryOptions: CategoryI[];
setSelectedOperators: (operators: OperatorI[]) => void;
toggleOperator: (template: OperatorI) => void;
handleDragStart: (
e: React.DragEvent,
item: OperatorI,
source: "library"
) => void;
}
const OperatorLibrary: React.FC<OperatorLibraryProps> = ({
selectedOperators,
operatorList,
categoryOptions,
setSelectedOperators,
toggleOperator,
handleDragStart,
}) => {
const [searchTerm, setSearchTerm] = useState("");
const [showFavorites, setShowFavorites] = useState(false);
const [favorites, setFavorites] = useState<Set<string>>(new Set());
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
new Set([])
);
// 按分类分组
const groupedOperators = useMemo(() => {
const groups: { [key: string]: OperatorI[] } = {};
categoryOptions.forEach((cat: any) => {
groups[cat.name] = {
...cat,
operators: operatorList.filter((op) => op.categories?.includes(cat.id)),
};
});
if (selectedCategory && selectedCategory !== "all") {
Object.keys(groups).forEach((key) => {
if (groups[key].id !== selectedCategory) {
delete groups[key];
}
});
}
if (searchTerm) {
Object.keys(groups).forEach((key) => {
groups[key].operators = groups[key].operators.filter((operator) =>
operator.name.toLowerCase().includes(searchTerm.toLowerCase())
);
if (groups[key].operators.length === 0) {
delete groups[key];
}
});
}
if (showFavorites) {
Object.keys(groups).forEach((key) => {
groups[key].operators = groups[key].operators.filter((operator) =>
favorites.has(operator.id)
);
if (groups[key].operators.length === 0) {
delete groups[key];
}
});
}
setExpandedCategories(new Set(Object.keys(groups)));
return groups;
}, [categoryOptions, selectedCategory, searchTerm, showFavorites]);
// 过滤算子
const filteredOperators = useMemo(() => {
return Object.values(groupedOperators).flatMap(
(category) => category.operators
);
}, [groupedOperators]);
// 收藏切换
const toggleFavorite = (operatorId: string) => {
const newFavorites = new Set(favorites);
if (newFavorites.has(operatorId)) {
newFavorites.delete(operatorId);
} else {
newFavorites.add(operatorId);
}
setFavorites(newFavorites);
};
const fetchFavorite = async () => {
const newFavorites = new Set(favorites);
operatorList.forEach(item => {
item.isStar && newFavorites.add(item.id);
});
setFavorites(newFavorites);
}
useEffect(() => {
fetchFavorite()
}, [operatorList]);
// 全选分类算子
const handleSelectAll = (operators: OperatorI[]) => {
const newSelected = [...selectedOperators];
operators.forEach((operator) => {
if (!newSelected.some((op) => op.id === operator.id)) {
newSelected.push(operator);
}
});
setSelectedOperators(newSelected);
};
return (
<div className="w-1/4 h-full min-w-3xs flex flex-col">
<div className="pb-4 border-b border-gray-200">
<span className="flex items-center font-semibold text-base">
<Layers className="w-4 h-4 mr-2" />
({filteredOperators.length})
</span>
</div>
<div className="flex flex-col h-full pt-4 pr-4 overflow-hidden">
{/* 过滤器 */}
<div className="flex flex-wrap gap-2 border-b border-gray-100 pb-4">
<Input
prefix={<SearchOutlined />}
placeholder="搜索算子名称..."
value={searchTerm}
allowClear
onChange={(e) => setSearchTerm(e.target.value)}
/>
<Select
value={selectedCategory}
options={[{ label: "全部分类", value: "all" }, ...categoryOptions]}
onChange={setSelectedCategory}
className="flex-1"
placeholder="选择分类"
></Select>
<Tooltip title="只看收藏">
<span
className="cursor-pointer"
onClick={() => setShowFavorites(!showFavorites)}
>
{showFavorites ? (
<StarFilled style={{ color: "#FFD700" }} />
) : (
<StarOutlined />
)}
</span>
</Tooltip>
</div>
{/* 算子列表 */}
<div className="flex-1 overflow-auto">
{/* 分类算子 */}
<Collapse
ghost
activeKey={Array.from(expandedCategories)}
onChange={(keys) =>
setExpandedCategories(
new Set(Array.isArray(keys) ? keys : [keys])
)
}
>
{Object.entries(groupedOperators).map(([key, category]) => (
<Collapse.Panel
key={key}
header={
<div className="flex items-center justify-between w-full">
<span className="flex items-center gap-2">
<span>{category.name}</span>
<Tag>{category.operators.length}</Tag>
</span>
<Button
type="link"
size="small"
onClick={(e) => {
e.stopPropagation();
handleSelectAll(category.operators);
}}
>
</Button>
</div>
}
>
<OperatorList
selectedOperators={selectedOperators}
operators={category.operators}
favorites={favorites}
toggleOperator={toggleOperator}
onDragOperator={handleDragStart}
toggleFavorite={toggleFavorite}
/>
</Collapse.Panel>
))}
</Collapse>
{filteredOperators.length === 0 && (
<div className="text-center py-8 text-gray-400">
<SearchOutlined className="text-3xl mb-2 opacity-50" />
<div></div>
</div>
)}
</div>
</div>
</div>
);
};
export default OperatorLibrary;
import React, {useEffect, useMemo, useState} from "react";
import {Button, Card, Checkbox, Collapse, Input, Select, Tag, Tooltip,} from "antd";
import {SearchOutlined, StarFilled, StarOutlined} from "@ant-design/icons";
import {CategoryI, OperatorI} from "@/pages/OperatorMarket/operator.model";
import {Layers} from "lucide-react";
import {updateOperatorByIdUsingPut} from "@/pages/OperatorMarket/operator.api.ts";
interface OperatorListProps {
operators: OperatorI[];
favorites: Set<string>;
showPoppular?: boolean;
toggleFavorite: (id: string) => void;
toggleOperator: (operator: OperatorI) => void;
selectedOperators: OperatorI[];
onDragOperator: (
e: React.DragEvent,
item: OperatorI,
source: "library"
) => void;
}
const handleStar = async (operator: OperatorI, toggleFavorite: (id: string) => void) => {
const data = {
id: operator.id,
isStar: !operator.isStar
};
await updateOperatorByIdUsingPut(operator.id, data);
toggleFavorite(operator.id)
}
const OperatorList: React.FC<OperatorListProps> = ({
operators,
favorites,
toggleFavorite,
toggleOperator,
selectedOperators,
onDragOperator,
}) => (
<div className="grid grid-cols-1 gap-2">
{operators.map((operator) => {
// 判断是否已选
const isSelected = selectedOperators.some((op) => op.id === operator.id);
return (
<Card
size="small"
key={operator.id}
draggable
hoverable
onDragStart={(e) => onDragOperator(e, operator, "library")}
onClick={() => toggleOperator(operator)}
>
<div className="flex items-center justify-between">
<div className="flex flex-1 min-w-0 items-center gap-2">
<Checkbox checked={isSelected} />
<span className="flex-1 min-w-0 font-medium text-sm overflow-hidden text-ellipsis whitespace-nowrap">
{operator.name}
</span>
</div>
<span
className="cursor-pointer"
onClick={(event) => {
event.stopPropagation();
handleStar(operator, toggleFavorite);
}}
>
{favorites.has(operator.id) ? (
<StarFilled style={{ color: "#FFD700" }} />
) : (
<StarOutlined />
)}
</span>
</div>
</Card>
);
})}
</div>
);
interface OperatorLibraryProps {
selectedOperators: OperatorI[];
operatorList: OperatorI[];
categoryOptions: CategoryI[];
setSelectedOperators: (operators: OperatorI[]) => void;
toggleOperator: (template: OperatorI) => void;
handleDragStart: (
e: React.DragEvent,
item: OperatorI,
source: "library"
) => void;
}
const OperatorLibrary: React.FC<OperatorLibraryProps> = ({
selectedOperators,
operatorList,
categoryOptions,
setSelectedOperators,
toggleOperator,
handleDragStart,
}) => {
const [searchTerm, setSearchTerm] = useState("");
const [showFavorites, setShowFavorites] = useState(false);
const [favorites, setFavorites] = useState<Set<string>>(new Set());
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
new Set([])
);
// 按分类分组
const groupedOperators = useMemo(() => {
const groups: { [key: string]: OperatorI[] } = {};
categoryOptions.forEach((cat: any) => {
groups[cat.name] = {
...cat,
operators: operatorList.filter((op) => op.categories?.includes(cat.id)),
};
});
if (selectedCategory && selectedCategory !== "all") {
Object.keys(groups).forEach((key) => {
if (groups[key].id !== selectedCategory) {
delete groups[key];
}
});
}
if (searchTerm) {
Object.keys(groups).forEach((key) => {
groups[key].operators = groups[key].operators.filter((operator) =>
operator.name.toLowerCase().includes(searchTerm.toLowerCase())
);
if (groups[key].operators.length === 0) {
delete groups[key];
}
});
}
if (showFavorites) {
Object.keys(groups).forEach((key) => {
groups[key].operators = groups[key].operators.filter((operator) =>
favorites.has(operator.id)
);
if (groups[key].operators.length === 0) {
delete groups[key];
}
});
}
setExpandedCategories(new Set(Object.keys(groups)));
return groups;
}, [categoryOptions, selectedCategory, searchTerm, showFavorites]);
// 过滤算子
const filteredOperators = useMemo(() => {
return Object.values(groupedOperators).flatMap(
(category) => category.operators
);
}, [groupedOperators]);
// 收藏切换
const toggleFavorite = (operatorId: string) => {
const newFavorites = new Set(favorites);
if (newFavorites.has(operatorId)) {
newFavorites.delete(operatorId);
} else {
newFavorites.add(operatorId);
}
setFavorites(newFavorites);
};
const fetchFavorite = async () => {
const newFavorites = new Set(favorites);
operatorList.forEach(item => {
item.isStar && newFavorites.add(item.id);
});
setFavorites(newFavorites);
}
useEffect(() => {
fetchFavorite()
}, [operatorList]);
// 全选分类算子
const handleSelectAll = (operators: OperatorI[]) => {
const newSelected = [...selectedOperators];
operators.forEach((operator) => {
if (!newSelected.some((op) => op.id === operator.id)) {
newSelected.push(operator);
}
});
setSelectedOperators(newSelected);
};
return (
<div className="w-1/4 h-full min-w-3xs flex flex-col">
<div className="pb-4 border-b border-gray-200">
<span className="flex items-center font-semibold text-base">
<Layers className="w-4 h-4 mr-2" />
({filteredOperators.length})
</span>
</div>
<div className="flex flex-col h-full pt-4 pr-4 overflow-hidden">
{/* 过滤器 */}
<div className="flex flex-wrap gap-2 border-b border-gray-100 pb-4">
<Input
prefix={<SearchOutlined />}
placeholder="搜索算子名称..."
value={searchTerm}
allowClear
onChange={(e) => setSearchTerm(e.target.value)}
/>
<Select
value={selectedCategory}
options={[{ label: "全部分类", value: "all" }, ...categoryOptions]}
onChange={setSelectedCategory}
className="flex-1"
placeholder="选择分类"
></Select>
<Tooltip title="只看收藏">
<span
className="cursor-pointer"
onClick={() => setShowFavorites(!showFavorites)}
>
{showFavorites ? (
<StarFilled style={{ color: "#FFD700" }} />
) : (
<StarOutlined />
)}
</span>
</Tooltip>
</div>
{/* 算子列表 */}
<div className="flex-1 overflow-auto">
{/* 分类算子 */}
<Collapse
ghost
activeKey={Array.from(expandedCategories)}
onChange={(keys) =>
setExpandedCategories(
new Set(Array.isArray(keys) ? keys : [keys])
)
}
>
{Object.entries(groupedOperators).map(([key, category]) => (
<Collapse.Panel
key={key}
header={
<div className="flex items-center justify-between w-full">
<span className="flex items-center gap-2">
<span>{category.name}</span>
<Tag>{category.operators.length}</Tag>
</span>
<Button
type="link"
size="small"
onClick={(e) => {
e.stopPropagation();
handleSelectAll(category.operators);
}}
>
</Button>
</div>
}
>
<OperatorList
selectedOperators={selectedOperators}
operators={category.operators}
favorites={favorites}
toggleOperator={toggleOperator}
onDragOperator={handleDragStart}
toggleFavorite={toggleFavorite}
/>
</Collapse.Panel>
))}
</Collapse>
{filteredOperators.length === 0 && (
<div className="text-center py-8 text-gray-400">
<SearchOutlined className="text-3xl mb-2 opacity-50" />
<div></div>
</div>
)}
</div>
</div>
</div>
);
};
export default OperatorLibrary;

View File

@@ -1,213 +1,213 @@
import React, {useMemo, useState} from "react";
import { Card, Input, Tag, Select, Button } from "antd";
import { DeleteOutlined } from "@ant-design/icons";
import { CleansingTemplate } from "../../cleansing.model";
import { Workflow } from "lucide-react";
import {CategoryI, OperatorI} from "@/pages/OperatorMarket/operator.model";
interface OperatorFlowProps {
selectedOperators: OperatorI[];
configOperator: OperatorI | null;
templates: CleansingTemplate[];
currentTemplate: CleansingTemplate | null;
categoryOptions: [];
setCurrentTemplate: (template: CleansingTemplate | null) => void;
removeOperator: (id: string) => void;
setSelectedOperators: (operators: OperatorI[]) => void;
setConfigOperator: (operator: OperatorI | null) => void;
handleDragStart: (
e: React.DragEvent,
operator: OperatorI,
source: "sort"
) => void;
handleItemDragOver: (e: React.DragEvent, itemId: string) => void;
handleItemDragLeave: (e: React.DragEvent) => void;
handleItemDrop: (e: React.DragEvent, index: number) => void;
handleContainerDragOver: (e: React.DragEvent) => void;
handleContainerDragLeave: (e: React.DragEvent) => void;
handleDragEnd: (e: React.DragEvent) => void;
handleDropToContainer: (e: React.DragEvent) => void;
}
const OperatorFlow: React.FC<OperatorFlowProps> = ({
selectedOperators,
configOperator,
templates,
currentTemplate,
categoryOptions,
setSelectedOperators,
setConfigOperator,
removeOperator,
setCurrentTemplate,
handleDragStart,
handleItemDragLeave,
handleItemDragOver,
handleItemDrop,
handleContainerDragLeave,
handleDropToContainer,
handleDragEnd,
}) => {
const [editingIndex, setEditingIndex] = useState<string | null>(null);
const categoryMap = useMemo(() => {
const map: { [key: string]: CategoryI } = {};
categoryOptions.forEach((cat: any) => {
map[cat.id] = {
...cat,
};
});
return map;
}, [categoryOptions]);
// 添加编号修改处理函数
const handleIndexChange = (operatorId: string, newIndex: string) => {
const index = Number.parseInt(newIndex);
if (isNaN(index) || index < 1 || index > selectedOperators.length) {
return; // 无效输入,不处理
}
const currentIndex = selectedOperators.findIndex(
(op) => op.id === operatorId
);
if (currentIndex === -1) return;
const targetIndex = index - 1; // 转换为0基索引
if (currentIndex === targetIndex) return; // 位置没有变化
const newOperators = [...selectedOperators];
const [movedOperator] = newOperators.splice(currentIndex, 1);
newOperators.splice(targetIndex, 0, movedOperator);
setSelectedOperators(newOperators);
setEditingIndex(null);
};
return (
<div className="w-1/2 h-full min-w-xs flex-1 flex flex-col border-x border-gray-200">
{/* 工具栏 */}
<div className="px-4 pb-2 border-b border-gray-200">
<div className="flex flex-wrap gap-2 justify-between items-start">
<span className="font-semibold text-base flex items-center gap-2">
<Workflow className="w-5 h-5" />
({selectedOperators.length}){" "}
<Button
type="link"
size="small"
onClick={() => {
setConfigOperator(null);
setSelectedOperators([]);
}}
disabled={selectedOperators.length === 0}
>
</Button>
</span>
<Select
placeholder="选择模板"
className="min-w-64"
options={templates}
value={currentTemplate?.value}
onChange={(value) =>
setCurrentTemplate(
templates.find((t) => t.value === value) || null
)
}
></Select>
</div>
</div>
{/* 编排区域 */}
<div
className="flex-overflow-auto p-4 gap-2"
onDragOver={(e) => e.preventDefault()}
onDragLeave={handleContainerDragLeave}
onDrop={handleDropToContainer}
>
{selectedOperators.map((operator, index) => (
<Card
size="small"
key={operator.id}
style={
configOperator?.id === operator.id
? { borderColor: "#1677ff" }
: {}
}
hoverable
draggable
onDragStart={(e) => handleDragStart(e, operator, "sort")}
onDragEnd={handleDragEnd}
onDragOver={(e) => handleItemDragOver(e, operator.id)}
onDragLeave={handleItemDragLeave}
onDrop={(e) => handleItemDrop(e, index)}
onClick={() => setConfigOperator(operator)}
>
<div className="flex items-center gap-1">
{/* 可编辑编号 */}
<span></span>
{editingIndex === operator.id ? (
<Input
type="number"
min={1}
max={selectedOperators.length}
defaultValue={index + 1}
className="w-10 h-6 text-xs text-center"
style={{ width: 60 }}
autoFocus
onBlur={(e) => handleIndexChange(operator.id, e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter")
handleIndexChange(
operator.id,
(e.target as HTMLInputElement).value
);
else if (e.key === "Escape") setEditingIndex(null);
}}
onClick={(e) => e.stopPropagation()}
/>
) : (
<Tag
color="default"
onClick={(e) => {
e.stopPropagation();
setEditingIndex(operator.id);
}}
>
{index + 1}
</Tag>
)}
{/* 算子图标和名称 */}
<div className="flex items-center gap-2 min-w-0 flex-1">
<span className="font-medium text-sm truncate">
{operator.name}
</span>
</div>
{operator?.categories?.map((categoryId) => {
return <Tag color="default">{categoryMap[categoryId].name}</Tag>
})}
{/* 操作按钮 */}
<span
className="cursor-pointer text-red-500"
onClick={(e) => {
e.stopPropagation();
removeOperator(operator.id);
}}
>
<DeleteOutlined />
</span>
</div>
</Card>
))}
{selectedOperators.length === 0 && (
<div className="text-center py-16 text-gray-400 border-2 border-dashed border-gray-100 rounded-lg">
<Workflow className="w-full h-10 mb-4 opacity-50" />
<div className="text-lg font-medium mb-2"></div>
<div className="text-sm">
</div>
</div>
)}
</div>
</div>
);
};
export default OperatorFlow;
import React, {useMemo, useState} from "react";
import { Card, Input, Tag, Select, Button } from "antd";
import { DeleteOutlined } from "@ant-design/icons";
import { CleansingTemplate } from "../../cleansing.model";
import { Workflow } from "lucide-react";
import {CategoryI, OperatorI} from "@/pages/OperatorMarket/operator.model";
interface OperatorFlowProps {
selectedOperators: OperatorI[];
configOperator: OperatorI | null;
templates: CleansingTemplate[];
currentTemplate: CleansingTemplate | null;
categoryOptions: [];
setCurrentTemplate: (template: CleansingTemplate | null) => void;
removeOperator: (id: string) => void;
setSelectedOperators: (operators: OperatorI[]) => void;
setConfigOperator: (operator: OperatorI | null) => void;
handleDragStart: (
e: React.DragEvent,
operator: OperatorI,
source: "sort"
) => void;
handleItemDragOver: (e: React.DragEvent, itemId: string) => void;
handleItemDragLeave: (e: React.DragEvent) => void;
handleItemDrop: (e: React.DragEvent, index: number) => void;
handleContainerDragOver: (e: React.DragEvent) => void;
handleContainerDragLeave: (e: React.DragEvent) => void;
handleDragEnd: (e: React.DragEvent) => void;
handleDropToContainer: (e: React.DragEvent) => void;
}
const OperatorFlow: React.FC<OperatorFlowProps> = ({
selectedOperators,
configOperator,
templates,
currentTemplate,
categoryOptions,
setSelectedOperators,
setConfigOperator,
removeOperator,
setCurrentTemplate,
handleDragStart,
handleItemDragLeave,
handleItemDragOver,
handleItemDrop,
handleContainerDragLeave,
handleDropToContainer,
handleDragEnd,
}) => {
const [editingIndex, setEditingIndex] = useState<string | null>(null);
const categoryMap = useMemo(() => {
const map: { [key: string]: CategoryI } = {};
categoryOptions.forEach((cat: any) => {
map[cat.id] = {
...cat,
};
});
return map;
}, [categoryOptions]);
// 添加编号修改处理函数
const handleIndexChange = (operatorId: string, newIndex: string) => {
const index = Number.parseInt(newIndex);
if (isNaN(index) || index < 1 || index > selectedOperators.length) {
return; // 无效输入,不处理
}
const currentIndex = selectedOperators.findIndex(
(op) => op.id === operatorId
);
if (currentIndex === -1) return;
const targetIndex = index - 1; // 转换为0基索引
if (currentIndex === targetIndex) return; // 位置没有变化
const newOperators = [...selectedOperators];
const [movedOperator] = newOperators.splice(currentIndex, 1);
newOperators.splice(targetIndex, 0, movedOperator);
setSelectedOperators(newOperators);
setEditingIndex(null);
};
return (
<div className="w-1/2 h-full min-w-xs flex-1 flex flex-col border-x border-gray-200">
{/* 工具栏 */}
<div className="px-4 pb-2 border-b border-gray-200">
<div className="flex flex-wrap gap-2 justify-between items-start">
<span className="font-semibold text-base flex items-center gap-2">
<Workflow className="w-5 h-5" />
({selectedOperators.length}){" "}
<Button
type="link"
size="small"
onClick={() => {
setConfigOperator(null);
setSelectedOperators([]);
}}
disabled={selectedOperators.length === 0}
>
</Button>
</span>
<Select
placeholder="选择模板"
className="min-w-64"
options={templates}
value={currentTemplate?.value}
onChange={(value) =>
setCurrentTemplate(
templates.find((t) => t.value === value) || null
)
}
></Select>
</div>
</div>
{/* 编排区域 */}
<div
className="flex-overflow-auto p-4 gap-2"
onDragOver={(e) => e.preventDefault()}
onDragLeave={handleContainerDragLeave}
onDrop={handleDropToContainer}
>
{selectedOperators.map((operator, index) => (
<Card
size="small"
key={operator.id}
style={
configOperator?.id === operator.id
? { borderColor: "#1677ff" }
: {}
}
hoverable
draggable
onDragStart={(e) => handleDragStart(e, operator, "sort")}
onDragEnd={handleDragEnd}
onDragOver={(e) => handleItemDragOver(e, operator.id)}
onDragLeave={handleItemDragLeave}
onDrop={(e) => handleItemDrop(e, index)}
onClick={() => setConfigOperator(operator)}
>
<div className="flex items-center gap-1">
{/* 可编辑编号 */}
<span></span>
{editingIndex === operator.id ? (
<Input
type="number"
min={1}
max={selectedOperators.length}
defaultValue={index + 1}
className="w-10 h-6 text-xs text-center"
style={{ width: 60 }}
autoFocus
onBlur={(e) => handleIndexChange(operator.id, e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter")
handleIndexChange(
operator.id,
(e.target as HTMLInputElement).value
);
else if (e.key === "Escape") setEditingIndex(null);
}}
onClick={(e) => e.stopPropagation()}
/>
) : (
<Tag
color="default"
onClick={(e) => {
e.stopPropagation();
setEditingIndex(operator.id);
}}
>
{index + 1}
</Tag>
)}
{/* 算子图标和名称 */}
<div className="flex items-center gap-2 min-w-0 flex-1">
<span className="font-medium text-sm truncate">
{operator.name}
</span>
</div>
{operator?.categories?.map((categoryId) => {
return <Tag color="default">{categoryMap[categoryId].name}</Tag>
})}
{/* 操作按钮 */}
<span
className="cursor-pointer text-red-500"
onClick={(e) => {
e.stopPropagation();
removeOperator(operator.id);
}}
>
<DeleteOutlined />
</span>
</div>
</Card>
))}
{selectedOperators.length === 0 && (
<div className="text-center py-16 text-gray-400 border-2 border-dashed border-gray-100 rounded-lg">
<Workflow className="w-full h-10 mb-4 opacity-50" />
<div className="text-lg font-medium mb-2"></div>
<div className="text-sm">
</div>
</div>
)}
</div>
</div>
);
};
export default OperatorFlow;

View File

@@ -1,245 +1,245 @@
import React from "react";
import {
Input,
Select,
Radio,
Checkbox,
Form,
InputNumber,
Slider,
Space,
} from "antd";
import { ConfigI, OperatorI } from "@/pages/OperatorMarket/operator.model";
interface ParamConfigProps {
operator: OperatorI;
paramKey: string;
param: ConfigI;
onParamChange?: (operatorId: string, paramKey: string, value: any) => void;
}
const ParamConfig: React.FC<ParamConfigProps> = ({
operator,
paramKey,
param,
onParamChange,
}) => {
if (!param) return null;
let defaultVal: any = param.defaultVal;
if (param.type === "range") {
defaultVal = Array.isArray(param.defaultVal)
? param.defaultVal
: [
param?.properties?.[0]?.defaultVal,
param?.properties?.[1]?.defaultVal,
];
}
const [value, setValue] = React.useState(param.value || defaultVal);
const updateValue = (newValue: any) => {
setValue(newValue);
return onParamChange && onParamChange(operator.id, paramKey, newValue);
};
switch (param.type) {
case "input":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<Input
value={value}
onChange={(e) => updateValue(e.target.value)}
placeholder={`请输入${param.name}`}
className="w-full"
/>
</Form.Item>
);
case "select":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<Select
value={value}
onChange={updateValue}
options={(param.options || []).map((option: any) =>
typeof option === "string"
? { label: option, value: option }
: option
)}
placeholder={`请选择${param.name}`}
className="w-full"
/>
</Form.Item>
);
case "radio":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<Radio.Group
value={value}
onChange={(e) => updateValue(e.target.value)}
>
{(param.options || []).map((option: any) => (
<Radio
key={typeof option === "string" ? option : option.value}
value={typeof option === "string" ? option : option.value}
>
{typeof option === "string" ? option : option.label}
</Radio>
))}
</Radio.Group>
</Form.Item>
);
case "checkbox":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<Checkbox.Group
value={value}
onChange={updateValue}
options={param.options || []}
/>
</Form.Item>
);
case "slider":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<div className="flex items-center gap-1">
<Slider
value={value}
onChange={updateValue}
tooltip={{ open: true }}
marks={{
[param.min || 0]: `${param.min || 0}`,
[param.min + (param.max - param.min) / 2]: `${
(param.min + param.max) / 2
}`,
[param.max || 100]: `${param.max || 100}`,
}}
min={param.min || 0}
max={param.max || 100}
step={param.step || 1}
className="flex-1"
/>
<InputNumber
min={param.min || 0}
max={param.max || 100}
step={param.step || 1}
value={value}
onChange={updateValue}
style={{ width: 80 }}
/>
</div>
</Form.Item>
);
case "range": {
const min = param.min || param?.properties?.[0]?.min || 0;
const max = param.max || param?.properties?.[0]?.max || 1;
const step = param.step || param?.properties?.[0]?.step || 0.1;
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<Slider
value={Array.isArray(value) ? value : [value, value]}
onChange={(val) =>
updateValue(Array.isArray(val) ? val : [val, val])
}
range
min={min}
max={max }
step={step}
className="w-full"
/>
<Space>
<InputNumber
min={min}
max={max}
value={value[0]}
onChange={(val1) => updateValue([val1, value[1]])}
changeOnWheel
/>
~
<InputNumber
min={min}
max={max}
value={value[1]}
onChange={(val2) => updateValue([value[0], val2])}
changeOnWheel
/>
</Space>
</Form.Item>
);
}
case "inputNumber":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<InputNumber
value={value}
onChange={(val) => updateValue(val)}
placeholder={`请输入${param.name}`}
className="w-full"
min={param.min}
max={param.max}
step={param.step || 1}
/>
</Form.Item>
);
case "switch":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<Checkbox
checked={value as boolean}
onChange={(e) => updateValue(e.target.checked)}
>
{param.name}
</Checkbox>
</Form.Item>
);
case "multiple":
return (
<div className="pl-4 border-l border-gray-300">
{param.properties.map((subParam) => (
<ParamConfig
key={subParam.key}
operator={operator}
paramKey={subParam.key}
param={subParam}
onParamChange={onParamChange}
/>
))}
</div>
);
default:
return null;
}
};
export default ParamConfig;
import React from "react";
import {
Input,
Select,
Radio,
Checkbox,
Form,
InputNumber,
Slider,
Space,
} from "antd";
import { ConfigI, OperatorI } from "@/pages/OperatorMarket/operator.model";
interface ParamConfigProps {
operator: OperatorI;
paramKey: string;
param: ConfigI;
onParamChange?: (operatorId: string, paramKey: string, value: any) => void;
}
const ParamConfig: React.FC<ParamConfigProps> = ({
operator,
paramKey,
param,
onParamChange,
}) => {
if (!param) return null;
let defaultVal: any = param.defaultVal;
if (param.type === "range") {
defaultVal = Array.isArray(param.defaultVal)
? param.defaultVal
: [
param?.properties?.[0]?.defaultVal,
param?.properties?.[1]?.defaultVal,
];
}
const [value, setValue] = React.useState(param.value || defaultVal);
const updateValue = (newValue: any) => {
setValue(newValue);
return onParamChange && onParamChange(operator.id, paramKey, newValue);
};
switch (param.type) {
case "input":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<Input
value={value}
onChange={(e) => updateValue(e.target.value)}
placeholder={`请输入${param.name}`}
className="w-full"
/>
</Form.Item>
);
case "select":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<Select
value={value}
onChange={updateValue}
options={(param.options || []).map((option: any) =>
typeof option === "string"
? { label: option, value: option }
: option
)}
placeholder={`请选择${param.name}`}
className="w-full"
/>
</Form.Item>
);
case "radio":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<Radio.Group
value={value}
onChange={(e) => updateValue(e.target.value)}
>
{(param.options || []).map((option: any) => (
<Radio
key={typeof option === "string" ? option : option.value}
value={typeof option === "string" ? option : option.value}
>
{typeof option === "string" ? option : option.label}
</Radio>
))}
</Radio.Group>
</Form.Item>
);
case "checkbox":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<Checkbox.Group
value={value}
onChange={updateValue}
options={param.options || []}
/>
</Form.Item>
);
case "slider":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<div className="flex items-center gap-1">
<Slider
value={value}
onChange={updateValue}
tooltip={{ open: true }}
marks={{
[param.min || 0]: `${param.min || 0}`,
[param.min + (param.max - param.min) / 2]: `${
(param.min + param.max) / 2
}`,
[param.max || 100]: `${param.max || 100}`,
}}
min={param.min || 0}
max={param.max || 100}
step={param.step || 1}
className="flex-1"
/>
<InputNumber
min={param.min || 0}
max={param.max || 100}
step={param.step || 1}
value={value}
onChange={updateValue}
style={{ width: 80 }}
/>
</div>
</Form.Item>
);
case "range": {
const min = param.min || param?.properties?.[0]?.min || 0;
const max = param.max || param?.properties?.[0]?.max || 1;
const step = param.step || param?.properties?.[0]?.step || 0.1;
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<Slider
value={Array.isArray(value) ? value : [value, value]}
onChange={(val) =>
updateValue(Array.isArray(val) ? val : [val, val])
}
range
min={min}
max={max }
step={step}
className="w-full"
/>
<Space>
<InputNumber
min={min}
max={max}
value={value[0]}
onChange={(val1) => updateValue([val1, value[1]])}
changeOnWheel
/>
~
<InputNumber
min={min}
max={max}
value={value[1]}
onChange={(val2) => updateValue([value[0], val2])}
changeOnWheel
/>
</Space>
</Form.Item>
);
}
case "inputNumber":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<InputNumber
value={value}
onChange={(val) => updateValue(val)}
placeholder={`请输入${param.name}`}
className="w-full"
min={param.min}
max={param.max}
step={param.step || 1}
/>
</Form.Item>
);
case "switch":
return (
<Form.Item
label={param.name}
tooltip={param.description}
key={paramKey}
>
<Checkbox
checked={value as boolean}
onChange={(e) => updateValue(e.target.checked)}
>
{param.name}
</Checkbox>
</Form.Item>
);
case "multiple":
return (
<div className="pl-4 border-l border-gray-300">
{param.properties.map((subParam) => (
<ParamConfig
key={subParam.key}
operator={operator}
paramKey={subParam.key}
param={subParam}
onParamChange={onParamChange}
/>
))}
</div>
);
default:
return null;
}
};
export default ParamConfig;

View File

@@ -1,87 +1,87 @@
import { useDragOperators } from "./useDragOperators";
import { useOperatorOperations } from "./useOperatorOperations";
import OperatorConfig from "../components/OperatorConfig";
import OperatorLibrary from "../components/OperatorLibrary";
import OperatorOrchestration from "../components/OperatorOrchestration";
export function useCreateStepTwo() {
const {
operators,
selectedOperators,
templates,
currentTemplate,
configOperator,
currentStep,
categoryOptions,
handlePrev,
handleNext,
setCurrentTemplate,
setConfigOperator,
setSelectedOperators,
handleConfigChange,
toggleOperator,
removeOperator,
} = useOperatorOperations();
const {
handleDragStart,
handleDragEnd,
handleContainerDragOver,
handleContainerDragLeave,
handleItemDragOver,
handleItemDragLeave,
handleItemDrop,
handleDropToContainer,
} = useDragOperators({
operators: selectedOperators,
setOperators: setSelectedOperators,
});
const renderStepTwo = (
<div className="flex w-full h-full">
{/* 左侧算子库 */}
<OperatorLibrary
categoryOptions={categoryOptions}
selectedOperators={selectedOperators}
operatorList={operators}
setSelectedOperators={setSelectedOperators}
toggleOperator={toggleOperator}
handleDragStart={handleDragStart}
/>
{/* 中间算子编排区域 */}
<OperatorOrchestration
selectedOperators={selectedOperators}
configOperator={configOperator}
templates={templates}
currentTemplate={currentTemplate}
categoryOptions={categoryOptions}
setSelectedOperators={setSelectedOperators}
setConfigOperator={setConfigOperator}
setCurrentTemplate={setCurrentTemplate}
removeOperator={removeOperator}
handleDragStart={handleDragStart}
handleContainerDragLeave={handleContainerDragLeave}
handleContainerDragOver={handleContainerDragOver}
handleItemDragOver={handleItemDragOver}
handleItemDragLeave={handleItemDragLeave}
handleItemDrop={handleItemDrop}
handleDropToContainer={handleDropToContainer}
handleDragEnd={handleDragEnd}
/>
{/* 右侧参数配置面板 */}
<OperatorConfig
selectedOp={configOperator}
handleConfigChange={handleConfigChange}
/>
</div>
);
return {
renderStepTwo,
selectedOperators,
currentStep,
handlePrev,
handleNext,
};
}
import { useDragOperators } from "./useDragOperators";
import { useOperatorOperations } from "./useOperatorOperations";
import OperatorConfig from "../components/OperatorConfig";
import OperatorLibrary from "../components/OperatorLibrary";
import OperatorOrchestration from "../components/OperatorOrchestration";
export function useCreateStepTwo() {
const {
operators,
selectedOperators,
templates,
currentTemplate,
configOperator,
currentStep,
categoryOptions,
handlePrev,
handleNext,
setCurrentTemplate,
setConfigOperator,
setSelectedOperators,
handleConfigChange,
toggleOperator,
removeOperator,
} = useOperatorOperations();
const {
handleDragStart,
handleDragEnd,
handleContainerDragOver,
handleContainerDragLeave,
handleItemDragOver,
handleItemDragLeave,
handleItemDrop,
handleDropToContainer,
} = useDragOperators({
operators: selectedOperators,
setOperators: setSelectedOperators,
});
const renderStepTwo = (
<div className="flex w-full h-full">
{/* 左侧算子库 */}
<OperatorLibrary
categoryOptions={categoryOptions}
selectedOperators={selectedOperators}
operatorList={operators}
setSelectedOperators={setSelectedOperators}
toggleOperator={toggleOperator}
handleDragStart={handleDragStart}
/>
{/* 中间算子编排区域 */}
<OperatorOrchestration
selectedOperators={selectedOperators}
configOperator={configOperator}
templates={templates}
currentTemplate={currentTemplate}
categoryOptions={categoryOptions}
setSelectedOperators={setSelectedOperators}
setConfigOperator={setConfigOperator}
setCurrentTemplate={setCurrentTemplate}
removeOperator={removeOperator}
handleDragStart={handleDragStart}
handleContainerDragLeave={handleContainerDragLeave}
handleContainerDragOver={handleContainerDragOver}
handleItemDragOver={handleItemDragOver}
handleItemDragLeave={handleItemDragLeave}
handleItemDrop={handleItemDrop}
handleDropToContainer={handleDropToContainer}
handleDragEnd={handleDragEnd}
/>
{/* 右侧参数配置面板 */}
<OperatorConfig
selectedOp={configOperator}
handleConfigChange={handleConfigChange}
/>
</div>
);
return {
renderStepTwo,
selectedOperators,
currentStep,
handlePrev,
handleNext,
};
}

View File

@@ -1,158 +1,158 @@
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
import React, { useState } from "react";
export function useDragOperators({
operators,
setOperators,
}: {
operators: OperatorI[];
setOperators: (operators: OperatorI[]) => void;
}) {
const [draggingItem, setDraggingItem] = useState<OperatorI | null>(null);
const [draggingSource, setDraggingSource] = useState<
"library" | "sort" | null
>(null);
const [insertPosition, setInsertPosition] = useState<
"above" | "below" | null
>(null);
// 处理拖拽开始
const handleDragStart = (
e: React.DragEvent,
item: OperatorI,
source: "library" | "sort"
) => {
setDraggingItem({
...item,
originalId: item.id,
});
setDraggingSource(source);
e.dataTransfer.effectAllowed = "move";
};
// 处理拖拽结束
const handleDragEnd = () => {
setDraggingItem(null);
setInsertPosition(null);
};
// 处理容器拖拽经过
const handleContainerDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
// 处理容器拖拽离开
const handleContainerDragLeave = (e: React.DragEvent) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
setInsertPosition(null);
}
};
// 处理项目拖拽经过
const handleItemDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
const rect = e.currentTarget.getBoundingClientRect();
const mouseY = e.clientY;
const elementMiddle = rect.top + rect.height / 2;
// 判断鼠标在元素的上半部分还是下半部分
const newPosition = mouseY < elementMiddle ? "above" : "below";
setInsertPosition(newPosition);
};
// 处理项目拖拽离开
const handleItemDragLeave = (e: React.DragEvent) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
setInsertPosition(null);
}
};
// 处理放置到空白区域
const handleDropToContainer = (e: React.DragEvent) => {
e.preventDefault();
if (!draggingItem) return;
// 如果是从算子库拖拽过来的
if (draggingSource === "library") {
// 检查是否已存在
const exists = operators.some((item) => item.id === draggingItem.id);
if (!exists) {
setOperators([...operators, draggingItem]);
}
}
resetDragState();
};
// 处理放置到特定位置
const handleItemDrop = (e: React.DragEvent, targetIndex: number) => {
e.preventDefault();
e.stopPropagation();
if (!draggingItem) return;
// 从左侧拖拽到右侧的精确插入
if (draggingSource === "library") {
if (targetIndex !== -1) {
const insertIndex =
insertPosition === "above" ? targetIndex : targetIndex + 1;
// 检查是否已存在
const exists = operators.some((item) => item.id === draggingItem.id);
if (!exists) {
const newRightItems = [...operators];
newRightItems.splice(insertIndex, 0, draggingItem);
setOperators(newRightItems);
}
}
}
// 右侧容器内的重新排序
else if (draggingSource === "sort") {
const draggedIndex = operators.findIndex(
(item) => item.id === draggingItem.id
);
if (
draggedIndex !== -1 &&
targetIndex !== -1 &&
draggedIndex !== targetIndex
) {
const newItems = [...operators];
const [draggedItem] = newItems.splice(draggedIndex, 1);
// 计算正确的插入位置
let insertIndex =
insertPosition === "above" ? targetIndex : targetIndex + 1;
if (draggedIndex < insertIndex) {
insertIndex--; // 调整插入位置,因为已经移除了原元素
}
newItems.splice(insertIndex, 0, draggedItem);
setOperators(newItems);
}
}
resetDragState();
};
// 重置拖拽状态
const resetDragState = () => {
setDraggingItem(null);
setInsertPosition(null);
};
return {
handleDragStart,
handleDragEnd,
handleContainerDragOver,
handleContainerDragLeave,
handleItemDragOver,
handleItemDragLeave,
handleItemDrop,
handleDropToContainer,
};
}
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
import React, { useState } from "react";
export function useDragOperators({
operators,
setOperators,
}: {
operators: OperatorI[];
setOperators: (operators: OperatorI[]) => void;
}) {
const [draggingItem, setDraggingItem] = useState<OperatorI | null>(null);
const [draggingSource, setDraggingSource] = useState<
"library" | "sort" | null
>(null);
const [insertPosition, setInsertPosition] = useState<
"above" | "below" | null
>(null);
// 处理拖拽开始
const handleDragStart = (
e: React.DragEvent,
item: OperatorI,
source: "library" | "sort"
) => {
setDraggingItem({
...item,
originalId: item.id,
});
setDraggingSource(source);
e.dataTransfer.effectAllowed = "move";
};
// 处理拖拽结束
const handleDragEnd = () => {
setDraggingItem(null);
setInsertPosition(null);
};
// 处理容器拖拽经过
const handleContainerDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
// 处理容器拖拽离开
const handleContainerDragLeave = (e: React.DragEvent) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
setInsertPosition(null);
}
};
// 处理项目拖拽经过
const handleItemDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
const rect = e.currentTarget.getBoundingClientRect();
const mouseY = e.clientY;
const elementMiddle = rect.top + rect.height / 2;
// 判断鼠标在元素的上半部分还是下半部分
const newPosition = mouseY < elementMiddle ? "above" : "below";
setInsertPosition(newPosition);
};
// 处理项目拖拽离开
const handleItemDragLeave = (e: React.DragEvent) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
setInsertPosition(null);
}
};
// 处理放置到空白区域
const handleDropToContainer = (e: React.DragEvent) => {
e.preventDefault();
if (!draggingItem) return;
// 如果是从算子库拖拽过来的
if (draggingSource === "library") {
// 检查是否已存在
const exists = operators.some((item) => item.id === draggingItem.id);
if (!exists) {
setOperators([...operators, draggingItem]);
}
}
resetDragState();
};
// 处理放置到特定位置
const handleItemDrop = (e: React.DragEvent, targetIndex: number) => {
e.preventDefault();
e.stopPropagation();
if (!draggingItem) return;
// 从左侧拖拽到右侧的精确插入
if (draggingSource === "library") {
if (targetIndex !== -1) {
const insertIndex =
insertPosition === "above" ? targetIndex : targetIndex + 1;
// 检查是否已存在
const exists = operators.some((item) => item.id === draggingItem.id);
if (!exists) {
const newRightItems = [...operators];
newRightItems.splice(insertIndex, 0, draggingItem);
setOperators(newRightItems);
}
}
}
// 右侧容器内的重新排序
else if (draggingSource === "sort") {
const draggedIndex = operators.findIndex(
(item) => item.id === draggingItem.id
);
if (
draggedIndex !== -1 &&
targetIndex !== -1 &&
draggedIndex !== targetIndex
) {
const newItems = [...operators];
const [draggedItem] = newItems.splice(draggedIndex, 1);
// 计算正确的插入位置
let insertIndex =
insertPosition === "above" ? targetIndex : targetIndex + 1;
if (draggedIndex < insertIndex) {
insertIndex--; // 调整插入位置,因为已经移除了原元素
}
newItems.splice(insertIndex, 0, draggedItem);
setOperators(newItems);
}
}
resetDragState();
};
// 重置拖拽状态
const resetDragState = () => {
setDraggingItem(null);
setInsertPosition(null);
};
return {
handleDragStart,
handleDragEnd,
handleContainerDragOver,
handleContainerDragLeave,
handleItemDragOver,
handleItemDragLeave,
handleItemDrop,
handleDropToContainer,
};
}

View File

@@ -1,169 +1,169 @@
import { useEffect, useState } from "react";
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
import { CleansingTemplate } from "../../cleansing.model";
import {queryCleaningTemplateByIdUsingGet, queryCleaningTemplatesUsingGet} from "../../cleansing.api";
import {
queryCategoryTreeUsingGet,
queryOperatorsUsingPost,
} from "@/pages/OperatorMarket/operator.api";
import {useParams} from "react-router";
export function useOperatorOperations() {
const { id = "" } = useParams();
const [currentStep, setCurrentStep] = useState(1);
const [operators, setOperators] = useState<OperatorI[]>([]);
const [selectedOperators, setSelectedOperators] = useState<OperatorI[]>([]);
const [configOperator, setConfigOperator] = useState<OperatorI | null>(null);
const [templates, setTemplates] = useState<CleansingTemplate[]>([]);
const [currentTemplate, setCurrentTemplate] =
useState<CleansingTemplate | null>(null);
// 将后端返回的算子数据映射为前端需要的格式
const mapOperator = (op: OperatorI) => {
const configs =
op.settings
? JSON.parse(op.settings)
: {};
const defaultParams: Record<string, string> = {};
Object.keys(configs).forEach((key) => {
const { value } = configs[key];
defaultParams[key] = value;
});
return {
...op,
defaultParams,
configs,
};
};
const [categoryOptions, setCategoryOptions] = useState([]);
const initOperators = async () => {
const [categoryRes, operatorRes] = await Promise.all([
queryCategoryTreeUsingGet(),
queryOperatorsUsingPost({ page: 0, size: 1000 }),
]);
const operators = operatorRes.data.content.map(mapOperator);
setOperators(operators || []);
const options = categoryRes.data.content.reduce((acc: any[], item: any) => {
const cats = item.categories.map((cat) => ({
...cat,
type: item.name,
label: cat.name,
value: cat.id,
icon: cat.icon,
operators: operators.filter((op) => op[item.name] === cat.name),
}));
acc.push(...cats);
return acc;
}, [] as { id: string; name: string; icon: React.ReactNode }[]);
setCategoryOptions(options);
};
const initTemplates = async () => {
if (id) {
const { data } = await queryCleaningTemplateByIdUsingGet(id);
const template = {
...data,
label: data.name,
value: data.id,
}
setTemplates([template])
setCurrentTemplate(template)
} else {
const { data } = await queryCleaningTemplatesUsingGet();
const newTemplates =
data.content?.map?.((item) => ({
...item,
label: item.name,
value: item.id,
})) || [];
setTemplates(newTemplates);
setCurrentTemplate(newTemplates?.[0])
}
};
useEffect(() => {
setSelectedOperators(currentTemplate?.instance?.map(mapOperator) || []);
}, [currentTemplate]);
useEffect(() => {
initTemplates();
initOperators();
}, []);
const toggleOperator = (operator: OperatorI) => {
const exist = selectedOperators.find((op) => op.id === operator.id);
if (exist) {
setSelectedOperators(
selectedOperators.filter((op) => op.id !== operator.id)
);
} else {
setSelectedOperators([...selectedOperators, { ...operator }]);
}
};
// 删除算子
const removeOperator = (id: string) => {
setSelectedOperators(selectedOperators.filter((op) => op.id !== id));
if (configOperator?.id === id) setConfigOperator(null);
};
// 配置算子参数变化
const handleConfigChange = (
operatorId: string,
paramKey: string,
value: any
) => {
setSelectedOperators((prev) =>
prev.map((op) =>
op.id === operatorId
? {
...op,
overrides: {
...(op?.overrides || op?.defaultParams),
[paramKey]: value,
},
}
: op
)
);
};
const handleNext = () => {
if (currentStep < 2) {
setCurrentStep(currentStep + 1);
}
};
const handlePrev = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
return {
currentStep,
templates,
currentTemplate,
configOperator,
categoryOptions,
setConfigOperator,
setCurrentTemplate,
setCurrentStep,
operators,
setOperators,
selectedOperators,
setSelectedOperators,
handleConfigChange,
toggleOperator,
removeOperator,
handleNext,
handlePrev,
};
}
import { useEffect, useState } from "react";
import { OperatorI } from "@/pages/OperatorMarket/operator.model";
import { CleansingTemplate } from "../../cleansing.model";
import {queryCleaningTemplateByIdUsingGet, queryCleaningTemplatesUsingGet} from "../../cleansing.api";
import {
queryCategoryTreeUsingGet,
queryOperatorsUsingPost,
} from "@/pages/OperatorMarket/operator.api";
import {useParams} from "react-router";
export function useOperatorOperations() {
const { id = "" } = useParams();
const [currentStep, setCurrentStep] = useState(1);
const [operators, setOperators] = useState<OperatorI[]>([]);
const [selectedOperators, setSelectedOperators] = useState<OperatorI[]>([]);
const [configOperator, setConfigOperator] = useState<OperatorI | null>(null);
const [templates, setTemplates] = useState<CleansingTemplate[]>([]);
const [currentTemplate, setCurrentTemplate] =
useState<CleansingTemplate | null>(null);
// 将后端返回的算子数据映射为前端需要的格式
const mapOperator = (op: OperatorI) => {
const configs =
op.settings
? JSON.parse(op.settings)
: {};
const defaultParams: Record<string, string> = {};
Object.keys(configs).forEach((key) => {
const { value } = configs[key];
defaultParams[key] = value;
});
return {
...op,
defaultParams,
configs,
};
};
const [categoryOptions, setCategoryOptions] = useState([]);
const initOperators = async () => {
const [categoryRes, operatorRes] = await Promise.all([
queryCategoryTreeUsingGet(),
queryOperatorsUsingPost({ page: 0, size: 1000 }),
]);
const operators = operatorRes.data.content.map(mapOperator);
setOperators(operators || []);
const options = categoryRes.data.content.reduce((acc: any[], item: any) => {
const cats = item.categories.map((cat) => ({
...cat,
type: item.name,
label: cat.name,
value: cat.id,
icon: cat.icon,
operators: operators.filter((op) => op[item.name] === cat.name),
}));
acc.push(...cats);
return acc;
}, [] as { id: string; name: string; icon: React.ReactNode }[]);
setCategoryOptions(options);
};
const initTemplates = async () => {
if (id) {
const { data } = await queryCleaningTemplateByIdUsingGet(id);
const template = {
...data,
label: data.name,
value: data.id,
}
setTemplates([template])
setCurrentTemplate(template)
} else {
const { data } = await queryCleaningTemplatesUsingGet();
const newTemplates =
data.content?.map?.((item) => ({
...item,
label: item.name,
value: item.id,
})) || [];
setTemplates(newTemplates);
setCurrentTemplate(newTemplates?.[0])
}
};
useEffect(() => {
setSelectedOperators(currentTemplate?.instance?.map(mapOperator) || []);
}, [currentTemplate]);
useEffect(() => {
initTemplates();
initOperators();
}, []);
const toggleOperator = (operator: OperatorI) => {
const exist = selectedOperators.find((op) => op.id === operator.id);
if (exist) {
setSelectedOperators(
selectedOperators.filter((op) => op.id !== operator.id)
);
} else {
setSelectedOperators([...selectedOperators, { ...operator }]);
}
};
// 删除算子
const removeOperator = (id: string) => {
setSelectedOperators(selectedOperators.filter((op) => op.id !== id));
if (configOperator?.id === id) setConfigOperator(null);
};
// 配置算子参数变化
const handleConfigChange = (
operatorId: string,
paramKey: string,
value: any
) => {
setSelectedOperators((prev) =>
prev.map((op) =>
op.id === operatorId
? {
...op,
overrides: {
...(op?.overrides || op?.defaultParams),
[paramKey]: value,
},
}
: op
)
);
};
const handleNext = () => {
if (currentStep < 2) {
setCurrentStep(currentStep + 1);
}
};
const handlePrev = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
return {
currentStep,
templates,
currentTemplate,
configOperator,
categoryOptions,
setConfigOperator,
setCurrentTemplate,
setCurrentStep,
operators,
setOperators,
selectedOperators,
setSelectedOperators,
handleConfigChange,
toggleOperator,
removeOperator,
handleNext,
handlePrev,
};
}

View File

@@ -1,223 +1,223 @@
import { useEffect, useState } from "react";
import {Breadcrumb, App, Tabs} from "antd";
import {
Play,
Pause,
Clock,
CheckCircle,
AlertCircle,
Trash2,
Activity, LayoutList,
} from "lucide-react";
import DetailHeader from "@/components/DetailHeader";
import { Link, useNavigate, useParams } from "react-router";
import {
deleteCleaningTaskByIdUsingDelete,
executeCleaningTaskUsingPost,
queryCleaningTaskByIdUsingGet, queryCleaningTaskLogByIdUsingGet, queryCleaningTaskResultByIdUsingGet,
stopCleaningTaskUsingPost,
} from "../cleansing.api";
import {mapTask, TaskStatusMap} from "../cleansing.const";
import {CleansingResult, TaskStatus} from "@/pages/DataCleansing/cleansing.model";
import BasicInfo from "./components/BasicInfo";
import OperatorTable from "./components/OperatorTable";
import FileTable from "./components/FileTable";
import LogsTable from "./components/LogsTable";
import {formatExecutionDuration} from "@/utils/unit.ts";
import {ReloadOutlined} from "@ant-design/icons";
// 任务详情页面组件
export default function CleansingTaskDetail() {
const { id = "" } = useParams(); // 获取动态路由参数
const { message } = App.useApp();
const navigate = useNavigate();
const fetchTaskDetail = async () => {
if (!id) return;
try {
const { data } = await queryCleaningTaskByIdUsingGet(id);
setTask(mapTask(data));
} catch (error) {
message.error("获取任务详情失败");
navigate("/data/cleansing");
}
};
const pauseTask = async () => {
await stopCleaningTaskUsingPost(id);
message.success("任务已暂停");
fetchTaskDetail();
};
const startTask = async () => {
await executeCleaningTaskUsingPost(id);
message.success("任务已启动");
fetchTaskDetail();
};
const deleteTask = async () => {
await deleteCleaningTaskByIdUsingDelete(id);
message.success("任务已删除");
navigate("/data/cleansing");
};
const [result, setResult] = useState<CleansingResult[]>();
const fetchTaskResult = async () => {
if (!id) return;
try {
const { data } = await queryCleaningTaskResultByIdUsingGet(id);
setResult(data);
} catch (error) {
message.error("获取清洗结果失败");
navigate("/data/cleansing/task-detail/" + id);
}
};
const [taskLog, setTaskLog] = useState();
const fetchTaskLog = async () => {
if (!id) return;
try {
const { data } = await queryCleaningTaskLogByIdUsingGet(id);
setTaskLog(data);
} catch (error) {
message.error("获取清洗日志失败");
navigate("/data/cleansing/task-detail/" + id);
}
};
const handleRefresh = async () => {
fetchTaskDetail();
{activeTab === "files" && await fetchTaskResult()}
{activeTab === "logs" && await fetchTaskLog()}
};
useEffect(() => {
fetchTaskDetail();
}, [id]);
const [task, setTask] = useState(null);
const [activeTab, setActiveTab] = useState("basic");
const headerData = {
...task,
icon: <LayoutList className="w-8 h-8" />,
status: TaskStatusMap[task?.status],
createdAt: task?.createdAt,
lastUpdated: task?.updatedAt,
};
const statistics = [
{
icon: <Clock className="w-4 h-4 text-blue-500" />,
label: "总耗时",
value: formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--",
},
{
icon: <CheckCircle className="w-4 h-4 text-green-500" />,
label: "成功文件",
value: task?.progress?.succeedFileNum || "0",
},
{
icon: <AlertCircle className="w-4 h-4 text-red-500" />,
label: "失败文件",
value: (task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress.failedFileNum :
task?.progress?.totalFileNum - task?.progress.succeedFileNum,
},
{
icon: <Activity className="w-4 h-4 text-purple-500" />,
label: "成功率",
value: task?.progress?.successRate ? task?.progress?.successRate + "%" : "--",
},
];
const operations = [
...(task?.status === TaskStatus.RUNNING
? [
{
key: "pause",
label: "暂停任务",
icon: <Pause className="w-4 h-4" />,
onClick: pauseTask,
},
]
: []),
...([TaskStatus.PENDING, TaskStatus.STOPPED, TaskStatus.FAILED].includes(task?.status?.value)
? [
{
key: "start",
label: "执行任务",
icon: <Play className="w-4 h-4" />,
onClick: startTask,
},
]
: []),
{
key: "refresh",
label: "更新任务",
icon: <ReloadOutlined className="w-4 h-4" />,
onClick: handleRefresh,
},
{
key: "delete",
label: "删除任务",
icon: <Trash2 className="w-4 h-4" />,
danger: true,
onClick: deleteTask,
},
];
const tabList = [
{
key: "basic",
label: "基本信息",
},
{
key: "operators",
label: "处理算子",
},
{
key: "files",
label: "处理文件",
},
{
key: "logs",
label: "运行日志",
},
];
const breadItems = [
{
title: <Link to="/data/cleansing"></Link>,
},
{
title: "清洗任务详情",
},
];
return (
<>
<Breadcrumb items={breadItems} />
<div className="mb-4 mt-4">
<DetailHeader
data={headerData}
statistics={statistics}
operations={operations}
/>
</div>
<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 flex-1 overflow-auto">
{activeTab === "basic" && (
<BasicInfo task={task} />
)}
{activeTab === "operators" && <OperatorTable task={task} />}
{activeTab === "files" && <FileTable result={result} fetchTaskResult={fetchTaskResult} />}
{activeTab === "logs" && <LogsTable taskLog={taskLog} fetchTaskLog={fetchTaskLog} />}
</div>
</div>
</>
);
}
import { useEffect, useState } from "react";
import {Breadcrumb, App, Tabs} from "antd";
import {
Play,
Pause,
Clock,
CheckCircle,
AlertCircle,
Trash2,
Activity, LayoutList,
} from "lucide-react";
import DetailHeader from "@/components/DetailHeader";
import { Link, useNavigate, useParams } from "react-router";
import {
deleteCleaningTaskByIdUsingDelete,
executeCleaningTaskUsingPost,
queryCleaningTaskByIdUsingGet, queryCleaningTaskLogByIdUsingGet, queryCleaningTaskResultByIdUsingGet,
stopCleaningTaskUsingPost,
} from "../cleansing.api";
import {mapTask, TaskStatusMap} from "../cleansing.const";
import {CleansingResult, TaskStatus} from "@/pages/DataCleansing/cleansing.model";
import BasicInfo from "./components/BasicInfo";
import OperatorTable from "./components/OperatorTable";
import FileTable from "./components/FileTable";
import LogsTable from "./components/LogsTable";
import {formatExecutionDuration} from "@/utils/unit.ts";
import {ReloadOutlined} from "@ant-design/icons";
// 任务详情页面组件
export default function CleansingTaskDetail() {
const { id = "" } = useParams(); // 获取动态路由参数
const { message } = App.useApp();
const navigate = useNavigate();
const fetchTaskDetail = async () => {
if (!id) return;
try {
const { data } = await queryCleaningTaskByIdUsingGet(id);
setTask(mapTask(data));
} catch (error) {
message.error("获取任务详情失败");
navigate("/data/cleansing");
}
};
const pauseTask = async () => {
await stopCleaningTaskUsingPost(id);
message.success("任务已暂停");
fetchTaskDetail();
};
const startTask = async () => {
await executeCleaningTaskUsingPost(id);
message.success("任务已启动");
fetchTaskDetail();
};
const deleteTask = async () => {
await deleteCleaningTaskByIdUsingDelete(id);
message.success("任务已删除");
navigate("/data/cleansing");
};
const [result, setResult] = useState<CleansingResult[]>();
const fetchTaskResult = async () => {
if (!id) return;
try {
const { data } = await queryCleaningTaskResultByIdUsingGet(id);
setResult(data);
} catch (error) {
message.error("获取清洗结果失败");
navigate("/data/cleansing/task-detail/" + id);
}
};
const [taskLog, setTaskLog] = useState();
const fetchTaskLog = async () => {
if (!id) return;
try {
const { data } = await queryCleaningTaskLogByIdUsingGet(id);
setTaskLog(data);
} catch (error) {
message.error("获取清洗日志失败");
navigate("/data/cleansing/task-detail/" + id);
}
};
const handleRefresh = async () => {
fetchTaskDetail();
{activeTab === "files" && await fetchTaskResult()}
{activeTab === "logs" && await fetchTaskLog()}
};
useEffect(() => {
fetchTaskDetail();
}, [id]);
const [task, setTask] = useState(null);
const [activeTab, setActiveTab] = useState("basic");
const headerData = {
...task,
icon: <LayoutList className="w-8 h-8" />,
status: TaskStatusMap[task?.status],
createdAt: task?.createdAt,
lastUpdated: task?.updatedAt,
};
const statistics = [
{
icon: <Clock className="w-4 h-4 text-blue-500" />,
label: "总耗时",
value: formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--",
},
{
icon: <CheckCircle className="w-4 h-4 text-green-500" />,
label: "成功文件",
value: task?.progress?.succeedFileNum || "0",
},
{
icon: <AlertCircle className="w-4 h-4 text-red-500" />,
label: "失败文件",
value: (task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress.failedFileNum :
task?.progress?.totalFileNum - task?.progress.succeedFileNum,
},
{
icon: <Activity className="w-4 h-4 text-purple-500" />,
label: "成功率",
value: task?.progress?.successRate ? task?.progress?.successRate + "%" : "--",
},
];
const operations = [
...(task?.status === TaskStatus.RUNNING
? [
{
key: "pause",
label: "暂停任务",
icon: <Pause className="w-4 h-4" />,
onClick: pauseTask,
},
]
: []),
...([TaskStatus.PENDING, TaskStatus.STOPPED, TaskStatus.FAILED].includes(task?.status?.value)
? [
{
key: "start",
label: "执行任务",
icon: <Play className="w-4 h-4" />,
onClick: startTask,
},
]
: []),
{
key: "refresh",
label: "更新任务",
icon: <ReloadOutlined className="w-4 h-4" />,
onClick: handleRefresh,
},
{
key: "delete",
label: "删除任务",
icon: <Trash2 className="w-4 h-4" />,
danger: true,
onClick: deleteTask,
},
];
const tabList = [
{
key: "basic",
label: "基本信息",
},
{
key: "operators",
label: "处理算子",
},
{
key: "files",
label: "处理文件",
},
{
key: "logs",
label: "运行日志",
},
];
const breadItems = [
{
title: <Link to="/data/cleansing"></Link>,
},
{
title: "清洗任务详情",
},
];
return (
<>
<Breadcrumb items={breadItems} />
<div className="mb-4 mt-4">
<DetailHeader
data={headerData}
statistics={statistics}
operations={operations}
/>
</div>
<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 flex-1 overflow-auto">
{activeTab === "basic" && (
<BasicInfo task={task} />
)}
{activeTab === "operators" && <OperatorTable task={task} />}
{activeTab === "files" && <FileTable result={result} fetchTaskResult={fetchTaskResult} />}
{activeTab === "logs" && <LogsTable taskLog={taskLog} fetchTaskLog={fetchTaskLog} />}
</div>
</div>
</>
);
}

View File

@@ -1,122 +1,122 @@
import { useEffect, useState } from "react";
import {Breadcrumb, App, Tabs} from "antd";
import {
Trash2,
LayoutList,
} from "lucide-react";
import DetailHeader from "@/components/DetailHeader";
import { Link, useNavigate, useParams } from "react-router";
import {
deleteCleaningTemplateByIdUsingDelete,
queryCleaningTemplateByIdUsingGet,
} from "../cleansing.api";
import {mapTemplate} from "../cleansing.const";
import OperatorTable from "./components/OperatorTable";
import {EditOutlined, ReloadOutlined, NumberOutlined} from "@ant-design/icons";
// 任务详情页面组件
export default function CleansingTemplateDetail() {
const { id = "" } = useParams(); // 获取动态路由参数
const { message } = App.useApp();
const navigate = useNavigate();
const [template, setTemplate] = useState();
const fetchTemplateDetail = async () => {
if (!id) return;
try {
const { data } = await queryCleaningTemplateByIdUsingGet(id);
setTemplate(mapTemplate(data));
} catch (error) {
message.error("获取任务详情失败");
navigate("/data/cleansing");
}
};
const deleteTemplate = async () => {
await deleteCleaningTemplateByIdUsingDelete(id);
message.success("模板已删除");
navigate("/data/cleansing");
};
const handleRefresh = async () => {
fetchTemplateDetail();
};
useEffect(() => {
fetchTemplateDetail();
}, [id]);
const [activeTab, setActiveTab] = useState("operators");
const headerData = {
...template,
icon: <LayoutList className="w-8 h-8" />,
createdAt: template?.createdAt,
lastUpdated: template?.updatedAt,
};
const statistics = [
{
icon: <NumberOutlined className="w-4 h-4 text-green-500" />,
label: "算子数量",
value: template?.instance?.length || 0,
},
];
const operations = [
{
key: "update",
label: "更新任务",
icon: <EditOutlined className="w-4 h-4" />,
onClick: () => navigate(`/data/cleansing/update-template/${id}`),
},
{
key: "refresh",
label: "更新任务",
icon: <ReloadOutlined className="w-4 h-4" />,
onClick: handleRefresh,
},
{
key: "delete",
label: "删除任务",
icon: <Trash2 className="w-4 h-4" />,
danger: true,
onClick: deleteTemplate,
},
];
const tabList = [
{
key: "operators",
label: "处理算子",
},
];
const breadItems = [
{
title: <Link to="/data/cleansing"></Link>,
},
{
title: "模板详情",
},
];
return (
<>
<Breadcrumb items={breadItems} />
<div className="mb-4 mt-4">
<DetailHeader
data={headerData}
statistics={statistics}
operations={operations}
/>
</div>
<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 flex-1 overflow-auto">
<OperatorTable task={template} />
</div>
</div>
</>
);
}
import { useEffect, useState } from "react";
import {Breadcrumb, App, Tabs} from "antd";
import {
Trash2,
LayoutList,
} from "lucide-react";
import DetailHeader from "@/components/DetailHeader";
import { Link, useNavigate, useParams } from "react-router";
import {
deleteCleaningTemplateByIdUsingDelete,
queryCleaningTemplateByIdUsingGet,
} from "../cleansing.api";
import {mapTemplate} from "../cleansing.const";
import OperatorTable from "./components/OperatorTable";
import {EditOutlined, ReloadOutlined, NumberOutlined} from "@ant-design/icons";
// 任务详情页面组件
export default function CleansingTemplateDetail() {
const { id = "" } = useParams(); // 获取动态路由参数
const { message } = App.useApp();
const navigate = useNavigate();
const [template, setTemplate] = useState();
const fetchTemplateDetail = async () => {
if (!id) return;
try {
const { data } = await queryCleaningTemplateByIdUsingGet(id);
setTemplate(mapTemplate(data));
} catch (error) {
message.error("获取任务详情失败");
navigate("/data/cleansing");
}
};
const deleteTemplate = async () => {
await deleteCleaningTemplateByIdUsingDelete(id);
message.success("模板已删除");
navigate("/data/cleansing");
};
const handleRefresh = async () => {
fetchTemplateDetail();
};
useEffect(() => {
fetchTemplateDetail();
}, [id]);
const [activeTab, setActiveTab] = useState("operators");
const headerData = {
...template,
icon: <LayoutList className="w-8 h-8" />,
createdAt: template?.createdAt,
lastUpdated: template?.updatedAt,
};
const statistics = [
{
icon: <NumberOutlined className="w-4 h-4 text-green-500" />,
label: "算子数量",
value: template?.instance?.length || 0,
},
];
const operations = [
{
key: "update",
label: "更新任务",
icon: <EditOutlined className="w-4 h-4" />,
onClick: () => navigate(`/data/cleansing/update-template/${id}`),
},
{
key: "refresh",
label: "更新任务",
icon: <ReloadOutlined className="w-4 h-4" />,
onClick: handleRefresh,
},
{
key: "delete",
label: "删除任务",
icon: <Trash2 className="w-4 h-4" />,
danger: true,
onClick: deleteTemplate,
},
];
const tabList = [
{
key: "operators",
label: "处理算子",
},
];
const breadItems = [
{
title: <Link to="/data/cleansing"></Link>,
},
{
title: "模板详情",
},
];
return (
<>
<Breadcrumb items={breadItems} />
<div className="mb-4 mt-4">
<DetailHeader
data={headerData}
statistics={statistics}
operations={operations}
/>
</div>
<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 flex-1 overflow-auto">
<OperatorTable task={template} />
</div>
</div>
</>
);
}

View File

@@ -1,138 +1,138 @@
import {CleansingTask, TaskStatus} from "@/pages/DataCleansing/cleansing.model";
import { Button, Card, Descriptions, Progress } from "antd";
import { Activity, AlertCircle, CheckCircle, Clock } from "lucide-react";
import { useNavigate } from "react-router";
import {formatExecutionDuration} from "@/utils/unit.ts";
export default function BasicInfo({ task }: { task: CleansingTask }) {
const navigate = useNavigate();
const descriptionItems = [
{
key: "id",
label: "任务ID",
children: <span className="font-mono">{task?.id}</span>,
},
{ key: "name", label: "任务名称", children: task?.name },
{
key: "dataset",
label: "源数据集",
children: (
<Button
style={{ paddingLeft: 0, marginLeft: 0 }}
type="link"
size="small"
onClick={() =>
navigate("/data/management/detail/" + task?.srcDatasetId)
}
>
{task?.srcDatasetName}
</Button>
),
},
{
key: "targetDataset",
label: "目标数据集",
children: (
<Button
style={{ paddingLeft: 0, marginLeft: 0 }}
type="link"
size="small"
onClick={() =>
navigate("/data/management/detail/" + task?.destDatasetId)
}
>
{task?.destDatasetName}
</Button>
),
},
{ key: "startTime", label: "开始时间", children: task?.startedAt },
{
key: "description",
label: "任务描述",
children: (
<span className="text-gray-600">{task?.description || "--"}</span>
),
span: 2,
},
];
return (
<>
{/* 执行摘要 */}
<Card className="mb-6">
<div className="grid grid-cols-4 gap-4">
<div className="text-center p-4 bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg">
<Clock className="w-8 h-8 text-blue-500 mb-2 mx-auto" />
<div className="text-xl font-bold text-blue-500">
{formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--"}
</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="text-center p-4 bg-gradient-to-br from-green-50 to-green-100 rounded-lg">
<CheckCircle className="w-8 h-8 text-green-500 mb-2 mx-auto" />
<div className="text-xl font-bold text-green-500">
{task?.progress?.succeedFileNum || "0"}
</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="text-center p-4 bg-gradient-to-br from-red-50 to-red-100 rounded-lg">
<AlertCircle className="w-8 h-8 text-red-500 mb-2 mx-auto" />
<div className="text-xl font-bold text-red-500">
{(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress.failedFileNum :
task?.progress?.totalFileNum - task?.progress.succeedFileNum}
</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="text-center p-4 bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg">
<Activity className="w-8 h-8 text-purple-500 mb-2 mx-auto" />
<div className="text-xl font-bold text-purple-500">
{task?.progress?.successRate ? task?.progress?.successRate + "%" : "--"}
</div>
<div className="text-sm text-gray-600"></div>
</div>
</div>
</Card>
{/* 基本信息 */}
<Card>
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<Descriptions
column={2}
bordered={false}
size="middle"
labelStyle={{ fontWeight: 500, color: "#555" }}
contentStyle={{ fontSize: 14 }}
items={descriptionItems}
></Descriptions>
</div>
{/* 处理进度 */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
{ task?.status?.value === TaskStatus.FAILED ?
<Progress percent={task?.progress?.process} size="small" status="exception" />
: <Progress percent={task?.progress?.process} size="small"/>
}
<div className="grid grid-cols-2 gap-4 text-sm mt-4">
<div className="flex items-center gap-2">
<span className="w-3 h-3 bg-green-500 rounded-full inline-block" />
<span>: {task?.progress?.succeedFileNum || "0"}</span>
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 bg-blue-500 rounded-full inline-block" />
<span>: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress?.totalFileNum - task?.progress.succeedFileNum : 0}</span>
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 bg-red-500 rounded-full inline-block" />
<span>: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress.failedFileNum :
task?.progress?.totalFileNum - task?.progress.succeedFileNum}</span>
</div>
</div>
</div>
</Card>
</>
);
}
import {CleansingTask, TaskStatus} from "@/pages/DataCleansing/cleansing.model";
import { Button, Card, Descriptions, Progress } from "antd";
import { Activity, AlertCircle, CheckCircle, Clock } from "lucide-react";
import { useNavigate } from "react-router";
import {formatExecutionDuration} from "@/utils/unit.ts";
export default function BasicInfo({ task }: { task: CleansingTask }) {
const navigate = useNavigate();
const descriptionItems = [
{
key: "id",
label: "任务ID",
children: <span className="font-mono">{task?.id}</span>,
},
{ key: "name", label: "任务名称", children: task?.name },
{
key: "dataset",
label: "源数据集",
children: (
<Button
style={{ paddingLeft: 0, marginLeft: 0 }}
type="link"
size="small"
onClick={() =>
navigate("/data/management/detail/" + task?.srcDatasetId)
}
>
{task?.srcDatasetName}
</Button>
),
},
{
key: "targetDataset",
label: "目标数据集",
children: (
<Button
style={{ paddingLeft: 0, marginLeft: 0 }}
type="link"
size="small"
onClick={() =>
navigate("/data/management/detail/" + task?.destDatasetId)
}
>
{task?.destDatasetName}
</Button>
),
},
{ key: "startTime", label: "开始时间", children: task?.startedAt },
{
key: "description",
label: "任务描述",
children: (
<span className="text-gray-600">{task?.description || "--"}</span>
),
span: 2,
},
];
return (
<>
{/* 执行摘要 */}
<Card className="mb-6">
<div className="grid grid-cols-4 gap-4">
<div className="text-center p-4 bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg">
<Clock className="w-8 h-8 text-blue-500 mb-2 mx-auto" />
<div className="text-xl font-bold text-blue-500">
{formatExecutionDuration(task?.startedAt, task?.finishedAt) || "--"}
</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="text-center p-4 bg-gradient-to-br from-green-50 to-green-100 rounded-lg">
<CheckCircle className="w-8 h-8 text-green-500 mb-2 mx-auto" />
<div className="text-xl font-bold text-green-500">
{task?.progress?.succeedFileNum || "0"}
</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="text-center p-4 bg-gradient-to-br from-red-50 to-red-100 rounded-lg">
<AlertCircle className="w-8 h-8 text-red-500 mb-2 mx-auto" />
<div className="text-xl font-bold text-red-500">
{(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress.failedFileNum :
task?.progress?.totalFileNum - task?.progress.succeedFileNum}
</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="text-center p-4 bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg">
<Activity className="w-8 h-8 text-purple-500 mb-2 mx-auto" />
<div className="text-xl font-bold text-purple-500">
{task?.progress?.successRate ? task?.progress?.successRate + "%" : "--"}
</div>
<div className="text-sm text-gray-600"></div>
</div>
</div>
</Card>
{/* 基本信息 */}
<Card>
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<Descriptions
column={2}
bordered={false}
size="middle"
labelStyle={{ fontWeight: 500, color: "#555" }}
contentStyle={{ fontSize: 14 }}
items={descriptionItems}
></Descriptions>
</div>
{/* 处理进度 */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
{ task?.status?.value === TaskStatus.FAILED ?
<Progress percent={task?.progress?.process} size="small" status="exception" />
: <Progress percent={task?.progress?.process} size="small"/>
}
<div className="grid grid-cols-2 gap-4 text-sm mt-4">
<div className="flex items-center gap-2">
<span className="w-3 h-3 bg-green-500 rounded-full inline-block" />
<span>: {task?.progress?.succeedFileNum || "0"}</span>
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 bg-blue-500 rounded-full inline-block" />
<span>: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress?.totalFileNum - task?.progress.succeedFileNum : 0}</span>
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 bg-red-500 rounded-full inline-block" />
<span>: {(task?.status.value === TaskStatus.RUNNING || task?.status.value === TaskStatus.PENDING) ?
task?.progress.failedFileNum :
task?.progress?.totalFileNum - task?.progress.succeedFileNum}</span>
</div>
</div>
</div>
</Card>
</>
);
}

View File

@@ -1,397 +1,397 @@
import {Button, Modal, Table, Badge, Input, Popover} from "antd";
import { Download } from "lucide-react";
import {useEffect, useState} from "react";
import {useParams} from "react-router";
import {TaskStatus} from "@/pages/DataCleansing/cleansing.model.ts";
import {TaskStatusMap} from "@/pages/DataCleansing/cleansing.const.tsx";
// 模拟文件列表数据
export default function FileTable({result, fetchTaskResult}) {
const { id = "" } = useParams();
const [showFileCompareDialog, setShowFileCompareDialog] = useState(false);
const [selectedFile, setSelectedFile] = useState<any>(null);
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
useEffect(() => {
fetchTaskResult();
}, [id]);
const handleSelectAllFiles = (checked: boolean) => {
if (checked) {
setSelectedFileIds(result.map((file) => file.instanceId));
} else {
setSelectedFileIds([]);
}
};
const handleSelectFile = (fileId: string, checked: boolean) => {
if (checked) {
setSelectedFileIds([...selectedFileIds, fileId]);
} else {
setSelectedFileIds(selectedFileIds.filter((id) => id !== fileId));
}
};
const handleViewFileCompare = (file: any) => {
setSelectedFile(file);
setShowFileCompareDialog(true);
};
const handleBatchDownload = () => {
// 实际下载逻辑
};
function formatFileSize(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
}
const fileColumns = [
{
title: (
<input
type="checkbox"
checked={
selectedFileIds.length === result?.length && result?.length > 0
}
onChange={(e) => handleSelectAllFiles(e.target.checked)}
className="w-4 h-4"
/>
),
dataIndex: "select",
key: "select",
width: 50,
render: (_text: string, record: any) => (
<input
type="checkbox"
checked={selectedFileIds.includes(record.id)}
onChange={(e) => handleSelectFile(record.id, e.target.checked)}
className="w-4 h-4"
/>
),
},
{
title: "文件名",
dataIndex: "srcName",
key: "srcName",
width: 200,
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
}: any) => (
<div className="p-4 w-64">
<Input
placeholder="搜索文件名"
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
className="mb-2"
/>
<div className="flex gap-2">
<Button size="small" onClick={() => confirm()}>
</Button>
<Button size="small" onClick={() => clearFilters()}>
</Button>
</div>
</div>
),
onFilter: (value: string, record: any) =>
record.srcName.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => (
<span>{text?.replace(/\.[^/.]+$/, "")}</span>
),
},
{
title: "清洗后文件名",
dataIndex: "destName",
key: "destName",
width: 200,
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
}: any) => (
<div className="p-4 w-64">
<Input
placeholder="搜索文件名"
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
className="mb-2"
/>
<div className="flex gap-2">
<Button size="small" onClick={() => confirm()}>
</Button>
<Button size="small" onClick={() => clearFilters()}>
</Button>
</div>
</div>
),
onFilter: (value: string, record: any) =>
record.destName.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => (
<span>{text?.replace(/\.[^/.]+$/, "")}</span>
),
},
{
title: "文件类型",
dataIndex: "srcType",
key: "srcType",
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
}: any) => (
<div className="p-4 w-64">
<Input
placeholder="搜索文件类型"
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
className="mb-2"
/>
<div className="flex gap-2">
<Button size="small" onClick={() => confirm()}>
</Button>
<Button size="small" onClick={() => clearFilters()}>
</Button>
</div>
</div>
),
onFilter: (value: string, record: any) =>
record.srcType.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => (
<span className="font-mono text-sm">{text}</span>
),
},
{
title: "清洗后文件类型",
dataIndex: "destType",
key: "destType",
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
}: any) => (
<div className="p-4 w-64">
<Input
placeholder="搜索文件类型"
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
className="mb-2"
/>
<div className="flex gap-2">
<Button size="small" onClick={() => confirm()}>
</Button>
<Button size="small" onClick={() => clearFilters()}>
</Button>
</div>
</div>
),
onFilter: (value: string, record: any) =>
record.destType.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => (
<span className="font-mono text-sm">{text}</span>
),
},
{
title: "清洗前大小",
dataIndex: "srcSize",
key: "srcSize",
sorter: (a: any, b: any) => {
const getSizeInBytes = (size: string) => {
if (!size || size === "-") return 0;
const num = Number.parseFloat(size);
if (size.includes("GB")) return num * 1024 * 1024 * 1024;
if (size.includes("MB")) return num * 1024 * 1024;
if (size.includes("KB")) return num * 1024;
return num;
};
return getSizeInBytes(a.originalSize) - getSizeInBytes(b.originalSize);
},
render: (number: number) => (
<span className="font-mono text-sm">{formatFileSize(number)}</span>
),
},
{
title: "清洗后大小",
dataIndex: "destSize",
key: "destSize",
sorter: (a: any, b: any) => {
const getSizeInBytes = (size: string) => {
if (!size || size === "-") return 0;
const num = Number.parseFloat(size);
if (size.includes("GB")) return num * 1024 * 1024 * 1024;
if (size.includes("MB")) return num * 1024 * 1024;
if (size.includes("KB")) return num * 1024;
return num;
};
return (
getSizeInBytes(a.processedSize) - getSizeInBytes(b.processedSize)
);
},
render: (number: number) => (
<span className="font-mono text-sm">{formatFileSize(number)}</span>
),
},
{
title: "状态",
dataIndex: "status",
key: "status",
filters: [
{ text: "已完成", value: "COMPLETED" },
{ text: "失败", value: "FAILED" },
],
onFilter: (value: string, record: any) => record.status === value,
render: (status: string) => (
<Badge
status={
status === "COMPLETED"
? "success"
: "error"
}
text={TaskStatusMap[status as TaskStatus].label}
/>
),
},
{
title: "操作",
key: "action",
width: 200,
render: (_text: string, record: any) => (
<div className="flex">
{record.status === "COMPLETED" ? (
<Button
type="link"
size="small"
onClick={() => handleViewFileCompare(record)}
>
</Button>
) : (
<Button
type="link"
size="small"
disabled
>
</Button>
)}
<Popover content="暂未开放">
<Button type="link" size="small" disabled></Button>
</Popover>
</div>
),
},
];
return (
<>
{selectedFileIds.length > 0 && (
<div className="mb-4 flex justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">
{selectedFileIds.length}
</span>
<Button
onClick={handleBatchDownload}
size="small"
type="primary"
icon={<Download className="w-4 h-4 mr-2" />}
>
</Button>
</div>
</div>
)}
<Table
columns={fileColumns}
dataSource={result}
pagination={{ pageSize: 10, showSizeChanger: true }}
size="middle"
rowKey="id"
/>
{/* 文件对比弹窗 */}
<Modal
open={showFileCompareDialog}
onCancel={() => setShowFileCompareDialog(false)}
footer={null}
width={900}
title={<span> - {selectedFile?.fileName}</span>}
>
<div className="grid grid-cols-2 gap-6 py-6">
<div>
<h4 className="font-medium text-gray-900"></h4>
<div className="border border-gray-200 rounded-lg p-6 bg-gray-50 min-h-48 flex items-center justify-center">
<div className="text-center text-gray-500">
<div className="w-16 h-16 bg-gray-300 rounded-lg mx-auto mb-2" />
<div className="text-sm"></div>
<div className="text-xs text-gray-400">
: {formatFileSize(selectedFile?.srcSize)}
</div>
</div>
</div>
<div className="text-sm text-gray-600 mt-3 space-y-1">
<div>
<span className="font-medium">:</span> {selectedFile?.srcType}
</div>
</div>
</div>
<div>
<h4 className="font-medium text-gray-900"></h4>
<div className="border border-gray-200 rounded-lg p-6 bg-gray-50 min-h-48 flex items-center justify-center">
<div className="text-center text-gray-500">
<div className="w-16 h-16 bg-blue-300 rounded-lg mx-auto mb-2" />
<div className="text-sm"></div>
<div className="text-xs text-gray-400">
: {formatFileSize(selectedFile?.destSize)}
</div>
</div>
</div>
<div className="text-sm text-gray-600 mt-3 space-y-1">
<div>
<span className="font-medium">:</span> {selectedFile?.destType}
</div>
</div>
</div>
</div>
<div className="border-t border-gray-200 mt-6 pt-4">
<h4 className="font-medium text-gray-900 mb-3"></h4>
<div className="grid grid-cols-3 gap-4 text-sm">
<div className="bg-green-50 p-4 rounded-lg">
<div className="font-medium text-green-700"></div>
<div className="text-green-600"> {(100 * (selectedFile?.srcSize - selectedFile?.destSize) / selectedFile?.srcSize).toFixed(2)}%</div>
</div>
</div>
</div>
</Modal>
</>
);
}
import {Button, Modal, Table, Badge, Input, Popover} from "antd";
import { Download } from "lucide-react";
import {useEffect, useState} from "react";
import {useParams} from "react-router";
import {TaskStatus} from "@/pages/DataCleansing/cleansing.model.ts";
import {TaskStatusMap} from "@/pages/DataCleansing/cleansing.const.tsx";
// 模拟文件列表数据
export default function FileTable({result, fetchTaskResult}) {
const { id = "" } = useParams();
const [showFileCompareDialog, setShowFileCompareDialog] = useState(false);
const [selectedFile, setSelectedFile] = useState<any>(null);
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
useEffect(() => {
fetchTaskResult();
}, [id]);
const handleSelectAllFiles = (checked: boolean) => {
if (checked) {
setSelectedFileIds(result.map((file) => file.instanceId));
} else {
setSelectedFileIds([]);
}
};
const handleSelectFile = (fileId: string, checked: boolean) => {
if (checked) {
setSelectedFileIds([...selectedFileIds, fileId]);
} else {
setSelectedFileIds(selectedFileIds.filter((id) => id !== fileId));
}
};
const handleViewFileCompare = (file: any) => {
setSelectedFile(file);
setShowFileCompareDialog(true);
};
const handleBatchDownload = () => {
// 实际下载逻辑
};
function formatFileSize(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
}
const fileColumns = [
{
title: (
<input
type="checkbox"
checked={
selectedFileIds.length === result?.length && result?.length > 0
}
onChange={(e) => handleSelectAllFiles(e.target.checked)}
className="w-4 h-4"
/>
),
dataIndex: "select",
key: "select",
width: 50,
render: (_text: string, record: any) => (
<input
type="checkbox"
checked={selectedFileIds.includes(record.id)}
onChange={(e) => handleSelectFile(record.id, e.target.checked)}
className="w-4 h-4"
/>
),
},
{
title: "文件名",
dataIndex: "srcName",
key: "srcName",
width: 200,
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
}: any) => (
<div className="p-4 w-64">
<Input
placeholder="搜索文件名"
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
className="mb-2"
/>
<div className="flex gap-2">
<Button size="small" onClick={() => confirm()}>
</Button>
<Button size="small" onClick={() => clearFilters()}>
</Button>
</div>
</div>
),
onFilter: (value: string, record: any) =>
record.srcName.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => (
<span>{text?.replace(/\.[^/.]+$/, "")}</span>
),
},
{
title: "清洗后文件名",
dataIndex: "destName",
key: "destName",
width: 200,
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
}: any) => (
<div className="p-4 w-64">
<Input
placeholder="搜索文件名"
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
className="mb-2"
/>
<div className="flex gap-2">
<Button size="small" onClick={() => confirm()}>
</Button>
<Button size="small" onClick={() => clearFilters()}>
</Button>
</div>
</div>
),
onFilter: (value: string, record: any) =>
record.destName.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => (
<span>{text?.replace(/\.[^/.]+$/, "")}</span>
),
},
{
title: "文件类型",
dataIndex: "srcType",
key: "srcType",
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
}: any) => (
<div className="p-4 w-64">
<Input
placeholder="搜索文件类型"
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
className="mb-2"
/>
<div className="flex gap-2">
<Button size="small" onClick={() => confirm()}>
</Button>
<Button size="small" onClick={() => clearFilters()}>
</Button>
</div>
</div>
),
onFilter: (value: string, record: any) =>
record.srcType.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => (
<span className="font-mono text-sm">{text}</span>
),
},
{
title: "清洗后文件类型",
dataIndex: "destType",
key: "destType",
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
}: any) => (
<div className="p-4 w-64">
<Input
placeholder="搜索文件类型"
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
className="mb-2"
/>
<div className="flex gap-2">
<Button size="small" onClick={() => confirm()}>
</Button>
<Button size="small" onClick={() => clearFilters()}>
</Button>
</div>
</div>
),
onFilter: (value: string, record: any) =>
record.destType.toLowerCase().includes(value.toLowerCase()),
render: (text: string) => (
<span className="font-mono text-sm">{text}</span>
),
},
{
title: "清洗前大小",
dataIndex: "srcSize",
key: "srcSize",
sorter: (a: any, b: any) => {
const getSizeInBytes = (size: string) => {
if (!size || size === "-") return 0;
const num = Number.parseFloat(size);
if (size.includes("GB")) return num * 1024 * 1024 * 1024;
if (size.includes("MB")) return num * 1024 * 1024;
if (size.includes("KB")) return num * 1024;
return num;
};
return getSizeInBytes(a.originalSize) - getSizeInBytes(b.originalSize);
},
render: (number: number) => (
<span className="font-mono text-sm">{formatFileSize(number)}</span>
),
},
{
title: "清洗后大小",
dataIndex: "destSize",
key: "destSize",
sorter: (a: any, b: any) => {
const getSizeInBytes = (size: string) => {
if (!size || size === "-") return 0;
const num = Number.parseFloat(size);
if (size.includes("GB")) return num * 1024 * 1024 * 1024;
if (size.includes("MB")) return num * 1024 * 1024;
if (size.includes("KB")) return num * 1024;
return num;
};
return (
getSizeInBytes(a.processedSize) - getSizeInBytes(b.processedSize)
);
},
render: (number: number) => (
<span className="font-mono text-sm">{formatFileSize(number)}</span>
),
},
{
title: "状态",
dataIndex: "status",
key: "status",
filters: [
{ text: "已完成", value: "COMPLETED" },
{ text: "失败", value: "FAILED" },
],
onFilter: (value: string, record: any) => record.status === value,
render: (status: string) => (
<Badge
status={
status === "COMPLETED"
? "success"
: "error"
}
text={TaskStatusMap[status as TaskStatus].label}
/>
),
},
{
title: "操作",
key: "action",
width: 200,
render: (_text: string, record: any) => (
<div className="flex">
{record.status === "COMPLETED" ? (
<Button
type="link"
size="small"
onClick={() => handleViewFileCompare(record)}
>
</Button>
) : (
<Button
type="link"
size="small"
disabled
>
</Button>
)}
<Popover content="暂未开放">
<Button type="link" size="small" disabled></Button>
</Popover>
</div>
),
},
];
return (
<>
{selectedFileIds.length > 0 && (
<div className="mb-4 flex justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">
{selectedFileIds.length}
</span>
<Button
onClick={handleBatchDownload}
size="small"
type="primary"
icon={<Download className="w-4 h-4 mr-2" />}
>
</Button>
</div>
</div>
)}
<Table
columns={fileColumns}
dataSource={result}
pagination={{ pageSize: 10, showSizeChanger: true }}
size="middle"
rowKey="id"
/>
{/* 文件对比弹窗 */}
<Modal
open={showFileCompareDialog}
onCancel={() => setShowFileCompareDialog(false)}
footer={null}
width={900}
title={<span> - {selectedFile?.fileName}</span>}
>
<div className="grid grid-cols-2 gap-6 py-6">
<div>
<h4 className="font-medium text-gray-900"></h4>
<div className="border border-gray-200 rounded-lg p-6 bg-gray-50 min-h-48 flex items-center justify-center">
<div className="text-center text-gray-500">
<div className="w-16 h-16 bg-gray-300 rounded-lg mx-auto mb-2" />
<div className="text-sm"></div>
<div className="text-xs text-gray-400">
: {formatFileSize(selectedFile?.srcSize)}
</div>
</div>
</div>
<div className="text-sm text-gray-600 mt-3 space-y-1">
<div>
<span className="font-medium">:</span> {selectedFile?.srcType}
</div>
</div>
</div>
<div>
<h4 className="font-medium text-gray-900"></h4>
<div className="border border-gray-200 rounded-lg p-6 bg-gray-50 min-h-48 flex items-center justify-center">
<div className="text-center text-gray-500">
<div className="w-16 h-16 bg-blue-300 rounded-lg mx-auto mb-2" />
<div className="text-sm"></div>
<div className="text-xs text-gray-400">
: {formatFileSize(selectedFile?.destSize)}
</div>
</div>
</div>
<div className="text-sm text-gray-600 mt-3 space-y-1">
<div>
<span className="font-medium">:</span> {selectedFile?.destType}
</div>
</div>
</div>
</div>
<div className="border-t border-gray-200 mt-6 pt-4">
<h4 className="font-medium text-gray-900 mb-3"></h4>
<div className="grid grid-cols-3 gap-4 text-sm">
<div className="bg-green-50 p-4 rounded-lg">
<div className="font-medium text-green-700"></div>
<div className="text-green-600"> {(100 * (selectedFile?.srcSize - selectedFile?.destSize) / selectedFile?.srcSize).toFixed(2)}%</div>
</div>
</div>
</div>
</Modal>
</>
);
}

View File

@@ -1,43 +1,43 @@
import {useEffect} from "react";
import {useParams} from "react-router";
import {FileClock} from "lucide-react";
export default function LogsTable({taskLog, fetchTaskLog} : {taskLog: any[], fetchTaskLog: () => Promise<any>}) {
const { id = "" } = useParams();
useEffect(() => {
fetchTaskLog();
}, [id]);
return taskLog?.length > 0 ? (
<>
<div className="text-gray-300 p-4 border border-gray-700 bg-gray-800 rounded-lg">
<div className="font-mono text-sm">
{taskLog?.map?.((log, index) => (
<div key={index} className="flex gap-3">
<span
className={`min-w-20 ${
log.level === "ERROR" || log.level === "FATAL"
? "text-red-500"
: log.level === "WARNING" || log.level === "WARN"
? "text-yellow-500"
: "text-green-500"
}`}
>
[{log.level}]
</span>
<span className="text-gray-100">{log.message}</span>
</div>
))}
</div>
</div>
</>
) : (
<div className="text-center py-12">
<FileClock className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
</h3>
</div>
);
}
import {useEffect} from "react";
import {useParams} from "react-router";
import {FileClock} from "lucide-react";
export default function LogsTable({taskLog, fetchTaskLog} : {taskLog: any[], fetchTaskLog: () => Promise<any>}) {
const { id = "" } = useParams();
useEffect(() => {
fetchTaskLog();
}, [id]);
return taskLog?.length > 0 ? (
<>
<div className="text-gray-300 p-4 border border-gray-700 bg-gray-800 rounded-lg">
<div className="font-mono text-sm">
{taskLog?.map?.((log, index) => (
<div key={index} className="flex gap-3">
<span
className={`min-w-20 ${
log.level === "ERROR" || log.level === "FATAL"
? "text-red-500"
: log.level === "WARNING" || log.level === "WARN"
? "text-yellow-500"
: "text-green-500"
}`}
>
[{log.level}]
</span>
<span className="text-gray-100">{log.message}</span>
</div>
))}
</div>
</div>
</>
) : (
<div className="text-center py-12">
<FileClock className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
</h3>
</div>
);
}

View File

@@ -1,25 +1,25 @@
import {Steps, Typography} from "antd";
import {useNavigate} from "react-router";
export default function OperatorTable({ task }: { task: any }) {
const navigate = useNavigate();
return task?.instance?.length > 0 && (
<>
<Steps
progressDot
direction="vertical"
items={Object.values(task?.instance).map((item) => ({
title: <Typography.Link
onClick={() => navigate(`/data/operator-market/plugin-detail/${item?.id}`)}
>
{item?.name}
</Typography.Link>,
description: item?.description,
status: "finish"
}))}
className="overflow-auto"
/>
</>
);
}
import {Steps, Typography} from "antd";
import {useNavigate} from "react-router";
export default function OperatorTable({ task }: { task: any }) {
const navigate = useNavigate();
return task?.instance?.length > 0 && (
<>
<Steps
progressDot
direction="vertical"
items={Object.values(task?.instance).map((item) => ({
title: <Typography.Link
onClick={() => navigate(`/data/operator-market/plugin-detail/${item?.id}`)}
>
{item?.name}
</Typography.Link>,
description: item?.description,
status: "finish"
}))}
className="overflow-auto"
/>
</>
);
}

View File

@@ -1,61 +1,61 @@
import { useEffect, useState } from "react";
import { Tabs, Button } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import { useNavigate } from "react-router";
import TaskList from "./components/TaskList";
import TemplateList from "./components/TemplateList";
import ProcessFlowDiagram from "./components/ProcessFlowDiagram";
import { useSearchParams } from "@/hooks/useSearchParams";
export default function DataProcessingPage() {
const navigate = useNavigate();
const urlParams = useSearchParams();
const [currentView, setCurrentView] = useState<"task" | "template">("task");
useEffect(() => {
if (urlParams.view) {
setCurrentView(urlParams.view);
}
}, [urlParams]);
return (
<div className="h-full flex flex-col gap-4">
{/* Header */}
<div className="flex justify-between items-center">
<h1 className="text-xl font-bold"></h1>
<div className="flex gap-2">
<Button
icon={<PlusOutlined />}
onClick={() => navigate("/data/cleansing/create-template")}
>
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate("/data/cleansing/create-task")}
>
</Button>
</div>
</div>
<ProcessFlowDiagram />
<Tabs
activeKey={currentView}
onChange={(key) => setCurrentView(key as any)}
items={[
{
key: "task",
label: "任务列表",
},
{
key: "template",
label: "模板管理",
},
]}
/>
{currentView === "task" && <TaskList />}
{currentView === "template" && <TemplateList />}
</div>
);
}
import { useEffect, useState } from "react";
import { Tabs, Button } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import { useNavigate } from "react-router";
import TaskList from "./components/TaskList";
import TemplateList from "./components/TemplateList";
import ProcessFlowDiagram from "./components/ProcessFlowDiagram";
import { useSearchParams } from "@/hooks/useSearchParams";
export default function DataProcessingPage() {
const navigate = useNavigate();
const urlParams = useSearchParams();
const [currentView, setCurrentView] = useState<"task" | "template">("task");
useEffect(() => {
if (urlParams.view) {
setCurrentView(urlParams.view);
}
}, [urlParams]);
return (
<div className="h-full flex flex-col gap-4">
{/* Header */}
<div className="flex justify-between items-center">
<h1 className="text-xl font-bold"></h1>
<div className="flex gap-2">
<Button
icon={<PlusOutlined />}
onClick={() => navigate("/data/cleansing/create-template")}
>
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate("/data/cleansing/create-task")}
>
</Button>
</div>
</div>
<ProcessFlowDiagram />
<Tabs
activeKey={currentView}
onChange={(key) => setCurrentView(key as any)}
items={[
{
key: "task",
label: "任务列表",
},
{
key: "template",
label: "模板管理",
},
]}
/>
{currentView === "task" && <TaskList />}
{currentView === "template" && <TemplateList />}
</div>
);
}

View File

@@ -1,86 +1,86 @@
import {
ArrowRight,
CheckCircle,
Database,
Play,
Settings,
Workflow,
Zap,
} from "lucide-react";
// 流程图组件
export default function ProcessFlowDiagram() {
const flowSteps = [
{
id: "start",
label: "开始",
type: "start",
icon: Play,
color: "bg-green-500",
},
{
id: "select",
label: "选择数据集",
type: "process",
icon: Database,
color: "bg-blue-500",
},
{
id: "config",
label: "基本配置",
type: "process",
icon: Settings,
color: "bg-purple-500",
},
{
id: "operators",
label: "算子编排",
type: "process",
icon: Workflow,
color: "bg-orange-500",
},
{
id: "execute",
label: "执行任务",
type: "process",
icon: Zap,
color: "bg-red-500",
},
{
id: "end",
label: "完成",
type: "end",
icon: CheckCircle,
color: "bg-green-500",
},
];
return (
<div className="border-card p-6">
<div className="w-full flex items-center justify-center">
<div className="w-full flex items-center space-x-12">
{flowSteps.map((step, index) => {
const IconComponent = step.icon;
return (
<div key={step.id} className="flex-1 flex items-center">
<div className="flex flex-col items-center w-full">
<div
className={`w-12 h-12 ${step.color} rounded-full flex items-center justify-center text-white shadow-lg`}
>
<IconComponent className="w-6 h-6" />
</div>
<span className="text-xs font-medium text-gray-700 mt-2 text-center max-w-16">
{step.label}
</span>
</div>
{index < flowSteps.length - 1 && (
<ArrowRight className="w-6 h-6 text-gray-400 mx-3" />
)}
</div>
);
})}
</div>
</div>
</div>
);
}
import {
ArrowRight,
CheckCircle,
Database,
Play,
Settings,
Workflow,
Zap,
} from "lucide-react";
// 流程图组件
export default function ProcessFlowDiagram() {
const flowSteps = [
{
id: "start",
label: "开始",
type: "start",
icon: Play,
color: "bg-green-500",
},
{
id: "select",
label: "选择数据集",
type: "process",
icon: Database,
color: "bg-blue-500",
},
{
id: "config",
label: "基本配置",
type: "process",
icon: Settings,
color: "bg-purple-500",
},
{
id: "operators",
label: "算子编排",
type: "process",
icon: Workflow,
color: "bg-orange-500",
},
{
id: "execute",
label: "执行任务",
type: "process",
icon: Zap,
color: "bg-red-500",
},
{
id: "end",
label: "完成",
type: "end",
icon: CheckCircle,
color: "bg-green-500",
},
];
return (
<div className="border-card p-6">
<div className="w-full flex items-center justify-center">
<div className="w-full flex items-center space-x-12">
{flowSteps.map((step, index) => {
const IconComponent = step.icon;
return (
<div key={step.id} className="flex-1 flex items-center">
<div className="flex flex-col items-center w-full">
<div
className={`w-12 h-12 ${step.color} rounded-full flex items-center justify-center text-white shadow-lg`}
>
<IconComponent className="w-6 h-6" />
</div>
<span className="text-xs font-medium text-gray-700 mt-2 text-center max-w-16">
{step.label}
</span>
</div>
{index < flowSteps.length - 1 && (
<ArrowRight className="w-6 h-6 text-gray-400 mx-3" />
)}
</div>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -1,308 +1,308 @@
import { useState } from "react";
import { Table, Progress, Badge, Button, Tooltip, Card, App } from "antd";
import {
PlayCircleOutlined,
PauseCircleOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import { SearchControls } from "@/components/SearchControls";
import CardView from "@/components/CardView";
import { useNavigate } from "react-router";
import { mapTask, TaskStatusMap } from "../../cleansing.const";
import {
TaskStatus,
type CleansingTask,
} from "@/pages/DataCleansing/cleansing.model";
import useFetchData from "@/hooks/useFetchData";
import {
deleteCleaningTaskByIdUsingDelete,
executeCleaningTaskUsingPost,
queryCleaningTasksUsingGet,
stopCleaningTaskUsingPost,
} from "../../cleansing.api";
export default function TaskList() {
const navigate = useNavigate();
const { message } = App.useApp();
const [viewMode, setViewMode] = useState<"card" | "list">("list");
const filterOptions = [
{
key: "status",
label: "状态",
options: [...Object.values(TaskStatusMap)],
},
];
const {
loading,
tableData,
pagination,
searchParams,
setSearchParams,
fetchData,
handleFiltersChange,
handleKeywordChange,
} = useFetchData(queryCleaningTasksUsingGet, mapTask);
const pauseTask = async (item: CleansingTask) => {
await stopCleaningTaskUsingPost(item.id);
message.success("任务已暂停");
fetchData();
};
const startTask = async (item: CleansingTask) => {
await executeCleaningTaskUsingPost(item.id);
message.success("任务已启动");
fetchData();
};
const deleteTask = async (item: CleansingTask) => {
await deleteCleaningTaskByIdUsingDelete(item.id);
message.success("任务已删除");
fetchData();
};
const taskOperations = (record: CleansingTask) => {
const isRunning = record.status?.value === TaskStatus.RUNNING;
const showStart = [
TaskStatus.PENDING,
TaskStatus.FAILED,
TaskStatus.STOPPED,
].includes(record.status?.value);
const pauseBtn = {
key: "pause",
label: "暂停",
icon: isRunning ? <PauseCircleOutlined /> : <PlayCircleOutlined />,
onClick: pauseTask, // implement pause/play logic
};
const startBtn = {
key: "start",
label: "启动",
icon: isRunning ? <PauseCircleOutlined /> : <PlayCircleOutlined />,
onClick: startTask, // implement pause/play logic
};
return [
...(isRunning
? [ pauseBtn ]
: []),
...(showStart
? [ startBtn ]
: []),
{
key: "delete",
label: "删除",
danger: true,
icon: <DeleteOutlined />,
onClick: deleteTask, // implement delete logic
},
];
};
const taskColumns = [
{
title: "任务名称",
dataIndex: "name",
key: "name",
fixed: "left",
width: 150,
ellipsis: true,
render: (_, task: CleansingTask) => {
return (
<Button
type="link"
onClick={() =>
navigate("/data/cleansing/task-detail/" + task.id)
}
>
{task.name}
</Button>
);
},
},
{
title: "任务ID",
dataIndex: "id",
key: "id",
width: 150,
ellipsis: true,
},
{
title: "源数据集",
dataIndex: "srcDatasetId",
key: "srcDatasetId",
width: 150,
ellipsis: true,
render: (_, record: CleansingTask) => {
return (
<Button
type="link"
onClick={() =>
navigate("/data/management/detail/" + record.srcDatasetId)
}
>
{record.srcDatasetName}
</Button>
);
},
},
{
title: "目标数据集",
dataIndex: "destDatasetId",
key: "destDatasetId",
width: 150,
ellipsis: true,
render: (_, record: CleansingTask) => {
return (
<Button
type="link"
onClick={() =>
navigate("/data/management/detail/" + record.destDatasetId)
}
>
{record.destDatasetName}
</Button>
);
},
},
{
title: "状态",
dataIndex: "status",
key: "status",
width: 100,
render: (status: any) => {
return <Badge color={status?.color} text={status?.label} />;
},
},
{
title: "进度",
dataIndex: "process",
key: "process",
width: 150,
render: (_, record: CleansingTask) => {
if (record?.status?.value == TaskStatus.FAILED) {
return <Progress percent={record?.progress?.process} size="small" status="exception" />;
}
return <Progress percent={record?.progress?.process} size="small"/>;
},
},
{
title: "已处理文件数",
dataIndex: "finishedFileNum",
key: "finishedFileNum",
width: 120,
align: "right",
ellipsis: true,
},
{
title: "总文件数",
dataIndex: "totalFileNum",
key: "totalFileNum",
width: 100,
align: "right",
ellipsis: true,
},
{
title: "执行耗时",
dataIndex: "duration",
key: "duration",
width: 100,
ellipsis: true,
},
{
title: "开始时间",
dataIndex: "startedAt",
key: "startedAt",
width: 180,
ellipsis: true,
},
{
title: "结束时间",
dataIndex: "finishedAt",
key: "finishedAt",
width: 180,
ellipsis: true,
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
width: 180,
ellipsis: true,
},
{
title: "数据量变化",
dataIndex: "dataSizeChange",
key: "dataSizeChange",
width: 180,
ellipsis: true,
render: (_: any, record: CleansingTask) => {
if (record.before !== undefined && record.after !== undefined) {
return `${record.before}${record.after}`;
}
return "-";
},
},
{
title: "操作",
key: "action",
fixed: "right",
render: (text: string, record: any) => (
<div className="flex gap-2">
{taskOperations(record).map((op) =>
op ? (
<Tooltip key={op.key} title={op.label}>
<Button
type="text"
icon={op.icon}
danger={op?.danger}
onClick={() => op.onClick(record)}
/>
</Tooltip>
) : null
)}
</div>
),
},
];
return (
<>
{/* Search and Filters */}
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange}
searchPlaceholder="搜索任务名称、描述"
filters={filterOptions}
onFiltersChange={handleFiltersChange}
viewMode={viewMode}
onViewModeChange={setViewMode}
showViewToggle={true}
onReload={fetchData}
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
/>
{/* Task List */}
{viewMode === "card" ? (
<CardView
data={tableData}
operations={taskOperations}
pagination={pagination}
onView={(tableData) => {
navigate("/data/cleansing/task-detail/" + tableData.id)
}}
/>
) : (
<Card>
<Table
columns={taskColumns}
dataSource={tableData}
rowKey="id"
loading={loading}
scroll={{ x: "max-content", y: "calc(100vh - 35rem)" }}
pagination={pagination}
/>
</Card>
)}
</>
);
}
import { useState } from "react";
import { Table, Progress, Badge, Button, Tooltip, Card, App } from "antd";
import {
PlayCircleOutlined,
PauseCircleOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import { SearchControls } from "@/components/SearchControls";
import CardView from "@/components/CardView";
import { useNavigate } from "react-router";
import { mapTask, TaskStatusMap } from "../../cleansing.const";
import {
TaskStatus,
type CleansingTask,
} from "@/pages/DataCleansing/cleansing.model";
import useFetchData from "@/hooks/useFetchData";
import {
deleteCleaningTaskByIdUsingDelete,
executeCleaningTaskUsingPost,
queryCleaningTasksUsingGet,
stopCleaningTaskUsingPost,
} from "../../cleansing.api";
export default function TaskList() {
const navigate = useNavigate();
const { message } = App.useApp();
const [viewMode, setViewMode] = useState<"card" | "list">("list");
const filterOptions = [
{
key: "status",
label: "状态",
options: [...Object.values(TaskStatusMap)],
},
];
const {
loading,
tableData,
pagination,
searchParams,
setSearchParams,
fetchData,
handleFiltersChange,
handleKeywordChange,
} = useFetchData(queryCleaningTasksUsingGet, mapTask);
const pauseTask = async (item: CleansingTask) => {
await stopCleaningTaskUsingPost(item.id);
message.success("任务已暂停");
fetchData();
};
const startTask = async (item: CleansingTask) => {
await executeCleaningTaskUsingPost(item.id);
message.success("任务已启动");
fetchData();
};
const deleteTask = async (item: CleansingTask) => {
await deleteCleaningTaskByIdUsingDelete(item.id);
message.success("任务已删除");
fetchData();
};
const taskOperations = (record: CleansingTask) => {
const isRunning = record.status?.value === TaskStatus.RUNNING;
const showStart = [
TaskStatus.PENDING,
TaskStatus.FAILED,
TaskStatus.STOPPED,
].includes(record.status?.value);
const pauseBtn = {
key: "pause",
label: "暂停",
icon: isRunning ? <PauseCircleOutlined /> : <PlayCircleOutlined />,
onClick: pauseTask, // implement pause/play logic
};
const startBtn = {
key: "start",
label: "启动",
icon: isRunning ? <PauseCircleOutlined /> : <PlayCircleOutlined />,
onClick: startTask, // implement pause/play logic
};
return [
...(isRunning
? [ pauseBtn ]
: []),
...(showStart
? [ startBtn ]
: []),
{
key: "delete",
label: "删除",
danger: true,
icon: <DeleteOutlined />,
onClick: deleteTask, // implement delete logic
},
];
};
const taskColumns = [
{
title: "任务名称",
dataIndex: "name",
key: "name",
fixed: "left",
width: 150,
ellipsis: true,
render: (_, task: CleansingTask) => {
return (
<Button
type="link"
onClick={() =>
navigate("/data/cleansing/task-detail/" + task.id)
}
>
{task.name}
</Button>
);
},
},
{
title: "任务ID",
dataIndex: "id",
key: "id",
width: 150,
ellipsis: true,
},
{
title: "源数据集",
dataIndex: "srcDatasetId",
key: "srcDatasetId",
width: 150,
ellipsis: true,
render: (_, record: CleansingTask) => {
return (
<Button
type="link"
onClick={() =>
navigate("/data/management/detail/" + record.srcDatasetId)
}
>
{record.srcDatasetName}
</Button>
);
},
},
{
title: "目标数据集",
dataIndex: "destDatasetId",
key: "destDatasetId",
width: 150,
ellipsis: true,
render: (_, record: CleansingTask) => {
return (
<Button
type="link"
onClick={() =>
navigate("/data/management/detail/" + record.destDatasetId)
}
>
{record.destDatasetName}
</Button>
);
},
},
{
title: "状态",
dataIndex: "status",
key: "status",
width: 100,
render: (status: any) => {
return <Badge color={status?.color} text={status?.label} />;
},
},
{
title: "进度",
dataIndex: "process",
key: "process",
width: 150,
render: (_, record: CleansingTask) => {
if (record?.status?.value == TaskStatus.FAILED) {
return <Progress percent={record?.progress?.process} size="small" status="exception" />;
}
return <Progress percent={record?.progress?.process} size="small"/>;
},
},
{
title: "已处理文件数",
dataIndex: "finishedFileNum",
key: "finishedFileNum",
width: 120,
align: "right",
ellipsis: true,
},
{
title: "总文件数",
dataIndex: "totalFileNum",
key: "totalFileNum",
width: 100,
align: "right",
ellipsis: true,
},
{
title: "执行耗时",
dataIndex: "duration",
key: "duration",
width: 100,
ellipsis: true,
},
{
title: "开始时间",
dataIndex: "startedAt",
key: "startedAt",
width: 180,
ellipsis: true,
},
{
title: "结束时间",
dataIndex: "finishedAt",
key: "finishedAt",
width: 180,
ellipsis: true,
},
{
title: "创建时间",
dataIndex: "createdAt",
key: "createdAt",
width: 180,
ellipsis: true,
},
{
title: "数据量变化",
dataIndex: "dataSizeChange",
key: "dataSizeChange",
width: 180,
ellipsis: true,
render: (_: any, record: CleansingTask) => {
if (record.before !== undefined && record.after !== undefined) {
return `${record.before}${record.after}`;
}
return "-";
},
},
{
title: "操作",
key: "action",
fixed: "right",
render: (text: string, record: any) => (
<div className="flex gap-2">
{taskOperations(record).map((op) =>
op ? (
<Tooltip key={op.key} title={op.label}>
<Button
type="text"
icon={op.icon}
danger={op?.danger}
onClick={() => op.onClick(record)}
/>
</Tooltip>
) : null
)}
</div>
),
},
];
return (
<>
{/* Search and Filters */}
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange}
searchPlaceholder="搜索任务名称、描述"
filters={filterOptions}
onFiltersChange={handleFiltersChange}
viewMode={viewMode}
onViewModeChange={setViewMode}
showViewToggle={true}
onReload={fetchData}
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
/>
{/* Task List */}
{viewMode === "card" ? (
<CardView
data={tableData}
operations={taskOperations}
pagination={pagination}
onView={(tableData) => {
navigate("/data/cleansing/task-detail/" + tableData.id)
}}
/>
) : (
<Card>
<Table
columns={taskColumns}
dataSource={tableData}
rowKey="id"
loading={loading}
scroll={{ x: "max-content", y: "calc(100vh - 35rem)" }}
pagination={pagination}
/>
</Card>
)}
</>
);
}

View File

@@ -1,156 +1,156 @@
import {DeleteOutlined, EditOutlined} from "@ant-design/icons";
import CardView from "@/components/CardView";
import {
deleteCleaningTemplateByIdUsingDelete, queryCleaningTemplatesUsingGet,
} from "../../cleansing.api";
import useFetchData from "@/hooks/useFetchData";
import {mapTemplate} from "../../cleansing.const";
import {App, Button, Card, Table, Tooltip} from "antd";
import {CleansingTemplate} from "../../cleansing.model";
import {SearchControls} from "@/components/SearchControls.tsx";
import {useNavigate} from "react-router";
import {useState} from "react";
export default function TemplateList() {
const navigate = useNavigate();
const { message } = App.useApp();
const [viewMode, setViewMode] = useState<"card" | "list">("list");
const {
loading,
tableData,
pagination,
searchParams,
setSearchParams,
fetchData,
handleFiltersChange,
handleKeywordChange,
} = useFetchData(queryCleaningTemplatesUsingGet, mapTemplate);
const templateOperations = () => {
return [
{
key: "update",
label: "编辑",
icon: <EditOutlined />,
onClick: (template: CleansingTemplate) => navigate(`/data/cleansing/update-template/${template.id}`)
},
{
key: "delete",
label: "删除",
danger: true,
icon: <DeleteOutlined />,
onClick: deleteTemplate, // implement delete logic
},
];
};
const templateColumns = [
{
title: "模板名称",
dataIndex: "name",
key: "name",
fixed: "left",
width: 150,
ellipsis: true,
render: (_, template: CleansingTemplate) => {
return (
<Button
type="link"
onClick={() =>
navigate("/data/cleansing/template-detail/" + template.id)
}
>
{template.name}
</Button>
);
}},
{
title: "模板ID",
dataIndex: "id",
key: "id",
fixed: "left",
width: 150,
},
{
title: "算子数量",
dataIndex: "num",
key: "num",
width: 100,
ellipsis: true,
render: (_, template: CleansingTemplate) => {
return template.instance?.length ?? 0;
},
},
{
title: "操作",
key: "action",
fixed: "right",
width: 20,
render: (text: string, record: any) => (
<div className="flex gap-2">
{templateOperations(record).map((op) =>
op ? (
<Tooltip key={op.key} title={op.label}>
<Button
type="text"
icon={op.icon}
danger={op?.danger}
onClick={() => op.onClick(record)}
/>
</Tooltip>
) : null
)}
</div>
),
},
]
const deleteTemplate = async (template: CleansingTemplate) => {
if (!template.id) {
return;
}
// 实现删除逻辑
await deleteCleaningTemplateByIdUsingDelete(template.id);
fetchData();
message.success("模板删除成功");
};
return (
<>
{/* Search and Filters */}
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange}
searchPlaceholder="搜索模板名称、描述"
onFiltersChange={handleFiltersChange}
viewMode={viewMode}
onViewModeChange={setViewMode}
showViewToggle={true}
onReload={fetchData}
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
/>
{viewMode === "card" ? (
<CardView
data={tableData}
operations={templateOperations}
pagination={pagination}
onView={(tableData) => {
navigate("/data/cleansing/template-detail/" + tableData.id)
}}
/>
) : (
<Card>
<Table
columns={templateColumns}
dataSource={tableData}
rowKey="id"
loading={loading}
scroll={{ x: "max-content", y: "calc(100vh - 35rem)" }}
pagination={pagination}
/>
</Card>
)}
</>
);
}
import {DeleteOutlined, EditOutlined} from "@ant-design/icons";
import CardView from "@/components/CardView";
import {
deleteCleaningTemplateByIdUsingDelete, queryCleaningTemplatesUsingGet,
} from "../../cleansing.api";
import useFetchData from "@/hooks/useFetchData";
import {mapTemplate} from "../../cleansing.const";
import {App, Button, Card, Table, Tooltip} from "antd";
import {CleansingTemplate} from "../../cleansing.model";
import {SearchControls} from "@/components/SearchControls.tsx";
import {useNavigate} from "react-router";
import {useState} from "react";
export default function TemplateList() {
const navigate = useNavigate();
const { message } = App.useApp();
const [viewMode, setViewMode] = useState<"card" | "list">("list");
const {
loading,
tableData,
pagination,
searchParams,
setSearchParams,
fetchData,
handleFiltersChange,
handleKeywordChange,
} = useFetchData(queryCleaningTemplatesUsingGet, mapTemplate);
const templateOperations = () => {
return [
{
key: "update",
label: "编辑",
icon: <EditOutlined />,
onClick: (template: CleansingTemplate) => navigate(`/data/cleansing/update-template/${template.id}`)
},
{
key: "delete",
label: "删除",
danger: true,
icon: <DeleteOutlined />,
onClick: deleteTemplate, // implement delete logic
},
];
};
const templateColumns = [
{
title: "模板名称",
dataIndex: "name",
key: "name",
fixed: "left",
width: 150,
ellipsis: true,
render: (_, template: CleansingTemplate) => {
return (
<Button
type="link"
onClick={() =>
navigate("/data/cleansing/template-detail/" + template.id)
}
>
{template.name}
</Button>
);
}},
{
title: "模板ID",
dataIndex: "id",
key: "id",
fixed: "left",
width: 150,
},
{
title: "算子数量",
dataIndex: "num",
key: "num",
width: 100,
ellipsis: true,
render: (_, template: CleansingTemplate) => {
return template.instance?.length ?? 0;
},
},
{
title: "操作",
key: "action",
fixed: "right",
width: 20,
render: (text: string, record: any) => (
<div className="flex gap-2">
{templateOperations(record).map((op) =>
op ? (
<Tooltip key={op.key} title={op.label}>
<Button
type="text"
icon={op.icon}
danger={op?.danger}
onClick={() => op.onClick(record)}
/>
</Tooltip>
) : null
)}
</div>
),
},
]
const deleteTemplate = async (template: CleansingTemplate) => {
if (!template.id) {
return;
}
// 实现删除逻辑
await deleteCleaningTemplateByIdUsingDelete(template.id);
fetchData();
message.success("模板删除成功");
};
return (
<>
{/* Search and Filters */}
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange}
searchPlaceholder="搜索模板名称、描述"
onFiltersChange={handleFiltersChange}
viewMode={viewMode}
onViewModeChange={setViewMode}
showViewToggle={true}
onReload={fetchData}
onClearFilters={() => setSearchParams({ ...searchParams, filter: {} })}
/>
{viewMode === "card" ? (
<CardView
data={tableData}
operations={templateOperations}
pagination={pagination}
onView={(tableData) => {
navigate("/data/cleansing/template-detail/" + tableData.id)
}}
/>
) : (
<Card>
<Table
columns={templateColumns}
dataSource={tableData}
rowKey="id"
loading={loading}
scroll={{ x: "max-content", y: "calc(100vh - 35rem)" }}
pagination={pagination}
/>
</Card>
)}
</>
);
}

View File

@@ -1,65 +1,65 @@
import { get, post, put, del } from "@/utils/request";
// 清洗任务相关接口
export function queryCleaningTasksUsingGet(params?: any) {
return get("/api/cleaning/tasks", params);
}
export function createCleaningTaskUsingPost(data: any) {
return post("/api/cleaning/tasks", data);
}
export function queryCleaningTaskByIdUsingGet(taskId: string | number) {
return get(`/api/cleaning/tasks/${taskId}`);
}
export function queryCleaningTaskResultByIdUsingGet(taskId: string | number) {
return get(`/api/cleaning/tasks/${taskId}/result`);
}
export function queryCleaningTaskLogByIdUsingGet(taskId: string | number) {
return get(`/api/cleaning/tasks/${taskId}/log`);
}
export function updateCleaningTaskByIdUsingPut(taskId: string | number, data: any) {
return put(`/api/cleaning/tasks/${taskId}`, data);
}
export function deleteCleaningTaskByIdUsingDelete(taskId: string | number) {
return del(`/api/cleaning/tasks/${taskId}`);
}
export function executeCleaningTaskUsingPost(taskId: string | number, data?: any) {
return post(`/api/cleaning/tasks/${taskId}/execute`, data);
}
export function stopCleaningTaskUsingPost(taskId: string | number, data?: any) {
return post(`/api/cleaning/tasks/${taskId}/stop`, data);
}
// 清洗模板相关接口
export function queryCleaningTemplatesUsingGet(params?: any) {
return get("/api/cleaning/templates", params);
}
export function createCleaningTemplateUsingPost(data: any) {
return post("/api/cleaning/templates", data);
}
export function queryCleaningTemplateByIdUsingGet(templateId: string | number) {
return get(`/api/cleaning/templates/${templateId}`);
}
export function updateCleaningTemplateByIdUsingPut(templateId: string | number, data: any) {
return put(`/api/cleaning/templates/${templateId}`, data);
}
export function deleteCleaningTemplateByIdUsingDelete(templateId: string | number) {
return del(`/api/cleaning/templates/${templateId}`);
}
import { get, post, put, del } from "@/utils/request";
// 清洗任务相关接口
export function queryCleaningTasksUsingGet(params?: any) {
return get("/api/cleaning/tasks", params);
}
export function createCleaningTaskUsingPost(data: any) {
return post("/api/cleaning/tasks", data);
}
export function queryCleaningTaskByIdUsingGet(taskId: string | number) {
return get(`/api/cleaning/tasks/${taskId}`);
}
export function queryCleaningTaskResultByIdUsingGet(taskId: string | number) {
return get(`/api/cleaning/tasks/${taskId}/result`);
}
export function queryCleaningTaskLogByIdUsingGet(taskId: string | number) {
return get(`/api/cleaning/tasks/${taskId}/log`);
}
export function updateCleaningTaskByIdUsingPut(taskId: string | number, data: any) {
return put(`/api/cleaning/tasks/${taskId}`, data);
}
export function deleteCleaningTaskByIdUsingDelete(taskId: string | number) {
return del(`/api/cleaning/tasks/${taskId}`);
}
export function executeCleaningTaskUsingPost(taskId: string | number, data?: any) {
return post(`/api/cleaning/tasks/${taskId}/execute`, data);
}
export function stopCleaningTaskUsingPost(taskId: string | number, data?: any) {
return post(`/api/cleaning/tasks/${taskId}/stop`, data);
}
// 清洗模板相关接口
export function queryCleaningTemplatesUsingGet(params?: any) {
return get("/api/cleaning/templates", params);
}
export function createCleaningTemplateUsingPost(data: any) {
return post("/api/cleaning/templates", data);
}
export function queryCleaningTemplateByIdUsingGet(templateId: string | number) {
return get(`/api/cleaning/templates/${templateId}`);
}
export function updateCleaningTemplateByIdUsingPut(templateId: string | number, data: any) {
return put(`/api/cleaning/templates/${templateId}`, data);
}
export function deleteCleaningTemplateByIdUsingDelete(templateId: string | number) {
return del(`/api/cleaning/templates/${templateId}`);
}

View File

@@ -1,137 +1,137 @@
import {
CleansingTask,
CleansingTemplate,
TaskStatus,
TemplateType,
} from "@/pages/DataCleansing/cleansing.model";
import {
formatBytes,
formatDateTime,
formatExecutionDuration,
} from "@/utils/unit";
import {
ClockCircleOutlined,
PlayCircleOutlined,
CheckCircleOutlined,
AlertOutlined,
PauseCircleOutlined,
} from "@ant-design/icons";
import { BrushCleaning, Layout } from "lucide-react";
export const templateTypesMap = {
[TemplateType.TEXT]: {
label: "文本",
value: TemplateType.TEXT,
icon: "📝",
description: "处理文本数据的清洗模板",
},
[TemplateType.IMAGE]: {
label: "图片",
value: TemplateType.IMAGE,
icon: "🖼️",
description: "处理图像数据的清洗模板",
},
[TemplateType.VIDEO]: {
value: TemplateType.VIDEO,
label: "视频",
icon: "🎥",
description: "处理视频数据的清洗模板",
},
[TemplateType.AUDIO]: {
value: TemplateType.AUDIO,
label: "音频",
icon: "🎵",
description: "处理音频数据的清洗模板",
},
[TemplateType.IMAGE2TEXT]: {
value: TemplateType.IMAGE2TEXT,
label: "图片转文本",
icon: "🔄",
description: "图像识别转文本的处理模板",
},
};
export const TaskStatusMap = {
[TaskStatus.PENDING]: {
label: "待处理",
value: TaskStatus.PENDING,
color: "gray",
icon: <ClockCircleOutlined />,
},
[TaskStatus.RUNNING]: {
label: "进行中",
value: TaskStatus.RUNNING,
color: "blue",
icon: <PlayCircleOutlined />,
},
[TaskStatus.COMPLETED]: {
label: "已完成",
value: TaskStatus.COMPLETED,
color: "green",
icon: <CheckCircleOutlined />,
},
[TaskStatus.FAILED]: {
label: "失败",
value: TaskStatus.FAILED,
color: "red",
icon: <AlertOutlined />,
},
[TaskStatus.STOPPED]: {
label: "已停止",
value: TaskStatus.STOPPED,
color: "orange",
icon: <PauseCircleOutlined />,
},
};
export const mapTask = (task: CleansingTask) => {
const duration = formatExecutionDuration(task.startedAt, task.finishedAt);
const before = formatBytes(task.beforeSize);
const after = formatBytes(task.afterSize);
const status = TaskStatusMap[task.status];
const finishedAt = formatDateTime(task.finishedAt);
const startedAt = formatDateTime(task.startedAt);
const createdAt = formatDateTime(task.createdAt);
return {
...task,
...task.progress,
createdAt,
startedAt,
finishedAt,
updatedAt: formatDateTime(
new Date(Math.max(...[
new Date(task.finishedAt).getTime(),
new Date(task.startedAt).getTime(),
new Date(task.createdAt).getTime()])).toISOString()),
icon: <BrushCleaning className="w-full h-full" />,
status,
duration,
before,
after,
statistics: [
{ label: "进度", value: `${task?.progress?.process || 0}%` },
{
label: "执行耗时",
value: duration,
},
{
label: "已处理文件数",
value: task?.progress?.finishedFileNum || 0,
},
{
label: "总文件数",
value: task?.progress?.totalFileNum || 0,
},
],
lastModified: formatDateTime(task.createdAt),
};
};
export const mapTemplate = (template: CleansingTemplate) => ({
...template,
createdAt: formatDateTime(template.createdAt),
updatedAt: formatDateTime(template.updatedAt),
icon: <Layout className="w-full h-full" />,
statistics: [{ label: "算子数量", value: template.instance?.length ?? 0 }],
lastModified: formatDateTime(template.updatedAt),
});
import {
CleansingTask,
CleansingTemplate,
TaskStatus,
TemplateType,
} from "@/pages/DataCleansing/cleansing.model";
import {
formatBytes,
formatDateTime,
formatExecutionDuration,
} from "@/utils/unit";
import {
ClockCircleOutlined,
PlayCircleOutlined,
CheckCircleOutlined,
AlertOutlined,
PauseCircleOutlined,
} from "@ant-design/icons";
import { BrushCleaning, Layout } from "lucide-react";
export const templateTypesMap = {
[TemplateType.TEXT]: {
label: "文本",
value: TemplateType.TEXT,
icon: "📝",
description: "处理文本数据的清洗模板",
},
[TemplateType.IMAGE]: {
label: "图片",
value: TemplateType.IMAGE,
icon: "🖼️",
description: "处理图像数据的清洗模板",
},
[TemplateType.VIDEO]: {
value: TemplateType.VIDEO,
label: "视频",
icon: "🎥",
description: "处理视频数据的清洗模板",
},
[TemplateType.AUDIO]: {
value: TemplateType.AUDIO,
label: "音频",
icon: "🎵",
description: "处理音频数据的清洗模板",
},
[TemplateType.IMAGE2TEXT]: {
value: TemplateType.IMAGE2TEXT,
label: "图片转文本",
icon: "🔄",
description: "图像识别转文本的处理模板",
},
};
export const TaskStatusMap = {
[TaskStatus.PENDING]: {
label: "待处理",
value: TaskStatus.PENDING,
color: "gray",
icon: <ClockCircleOutlined />,
},
[TaskStatus.RUNNING]: {
label: "进行中",
value: TaskStatus.RUNNING,
color: "blue",
icon: <PlayCircleOutlined />,
},
[TaskStatus.COMPLETED]: {
label: "已完成",
value: TaskStatus.COMPLETED,
color: "green",
icon: <CheckCircleOutlined />,
},
[TaskStatus.FAILED]: {
label: "失败",
value: TaskStatus.FAILED,
color: "red",
icon: <AlertOutlined />,
},
[TaskStatus.STOPPED]: {
label: "已停止",
value: TaskStatus.STOPPED,
color: "orange",
icon: <PauseCircleOutlined />,
},
};
export const mapTask = (task: CleansingTask) => {
const duration = formatExecutionDuration(task.startedAt, task.finishedAt);
const before = formatBytes(task.beforeSize);
const after = formatBytes(task.afterSize);
const status = TaskStatusMap[task.status];
const finishedAt = formatDateTime(task.finishedAt);
const startedAt = formatDateTime(task.startedAt);
const createdAt = formatDateTime(task.createdAt);
return {
...task,
...task.progress,
createdAt,
startedAt,
finishedAt,
updatedAt: formatDateTime(
new Date(Math.max(...[
new Date(task.finishedAt).getTime(),
new Date(task.startedAt).getTime(),
new Date(task.createdAt).getTime()])).toISOString()),
icon: <BrushCleaning className="w-full h-full" />,
status,
duration,
before,
after,
statistics: [
{ label: "进度", value: `${task?.progress?.process || 0}%` },
{
label: "执行耗时",
value: duration,
},
{
label: "已处理文件数",
value: task?.progress?.finishedFileNum || 0,
},
{
label: "总文件数",
value: task?.progress?.totalFileNum || 0,
},
],
lastModified: formatDateTime(task.createdAt),
};
};
export const mapTemplate = (template: CleansingTemplate) => ({
...template,
createdAt: formatDateTime(template.createdAt),
updatedAt: formatDateTime(template.updatedAt),
icon: <Layout className="w-full h-full" />,
statistics: [{ label: "算子数量", value: template.instance?.length ?? 0 }],
lastModified: formatDateTime(template.updatedAt),
});

View File

@@ -1,89 +1,89 @@
import { OperatorI } from "../OperatorMarket/operator.model";
export interface CleansingTask {
id: string;
name: string;
description?: string;
srcDatasetId: string;
srcDatasetName: string;
destDatasetId: string;
destDatasetName: string;
templateId: string;
templateName: string;
status: {
label: string;
value: TaskStatus;
color: string;
};
startedAt: string;
progress: {
finishedFileNum: number;
succeedFileNum: number;
failedFileNum: number;
process: 100;
totalFileNum: number;
successRate: 100;
};
instance: OperatorI[];
createdAt: string;
updatedAt: string;
finishedAt: string;
beforeSize?: number;
afterSize?: number;
}
export interface CleansingTemplate {
id: string;
name: string;
description?: string;
instance: OperatorI[];
createdAt: string;
updatedAt: string;
}
export enum RuleCategory {
DATA_VALIDATION = "DATA_VALIDATION",
MISSING_VALUE_HANDLING = "MISSING_VALUE_HANDLING",
OUTLIER_DETECTION = "OUTLIER_DETECTION",
DEDUPLICATION = "DEDUPLICATION",
FORMAT_STANDARDIZATION = "FORMAT_STANDARDIZATION",
TEXT_CLEANING = "TEXT_CLEANING",
CUSTOM = "CUSTOM",
}
export enum TaskStatus {
PENDING = "PENDING",
RUNNING = "RUNNING",
COMPLETED = "COMPLETED",
FAILED = "FAILED",
STOPPED = "STOPPED",
}
export interface RuleCondition {
field: string;
operator: string;
value: string;
logicOperator?: "AND" | "OR";
}
export enum TemplateType {
TEXT = "TEXT",
IMAGE = "IMAGE",
VIDEO = "VIDEO",
AUDIO = "AUDIO",
IMAGE2TEXT = "IMAGE2TEXT",
}
export interface CleansingResult {
instanceId: string;
srcFileId: string;
destFileId: string;
srcName: string;
destName: string;
srcType: string;
destType: string;
srcSize: number;
destSize: number;
status: string;
result: string;
import { OperatorI } from "../OperatorMarket/operator.model";
export interface CleansingTask {
id: string;
name: string;
description?: string;
srcDatasetId: string;
srcDatasetName: string;
destDatasetId: string;
destDatasetName: string;
templateId: string;
templateName: string;
status: {
label: string;
value: TaskStatus;
color: string;
};
startedAt: string;
progress: {
finishedFileNum: number;
succeedFileNum: number;
failedFileNum: number;
process: 100;
totalFileNum: number;
successRate: 100;
};
instance: OperatorI[];
createdAt: string;
updatedAt: string;
finishedAt: string;
beforeSize?: number;
afterSize?: number;
}
export interface CleansingTemplate {
id: string;
name: string;
description?: string;
instance: OperatorI[];
createdAt: string;
updatedAt: string;
}
export enum RuleCategory {
DATA_VALIDATION = "DATA_VALIDATION",
MISSING_VALUE_HANDLING = "MISSING_VALUE_HANDLING",
OUTLIER_DETECTION = "OUTLIER_DETECTION",
DEDUPLICATION = "DEDUPLICATION",
FORMAT_STANDARDIZATION = "FORMAT_STANDARDIZATION",
TEXT_CLEANING = "TEXT_CLEANING",
CUSTOM = "CUSTOM",
}
export enum TaskStatus {
PENDING = "PENDING",
RUNNING = "RUNNING",
COMPLETED = "COMPLETED",
FAILED = "FAILED",
STOPPED = "STOPPED",
}
export interface RuleCondition {
field: string;
operator: string;
value: string;
logicOperator?: "AND" | "OR";
}
export enum TemplateType {
TEXT = "TEXT",
IMAGE = "IMAGE",
VIDEO = "VIDEO",
AUDIO = "AUDIO",
IMAGE2TEXT = "IMAGE2TEXT",
}
export interface CleansingResult {
instanceId: string;
srcFileId: string;
destFileId: string;
srcName: string;
destName: string;
srcType: string;
destType: string;
srcSize: number;
destSize: number;
status: string;
result: string;
}