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,201 +1,201 @@
import { Button, App, Steps } from "antd";
import {
ArrowLeft,
CheckCircle,
Settings,
TagIcon,
Upload,
} from "lucide-react";
import { useNavigate, useParams } from "react-router";
import { useEffect, useState } from "react";
import UploadStep from "./components/UploadStep";
import ParsingStep from "./components/ParsingStep";
import ConfigureStep from "./components/ConfigureStep";
import PreviewStep from "./components/PreviewStep";
import { useFileSliceUpload } from "@/hooks/useSliceUpload";
import {
createOperatorUsingPost,
preUploadOperatorUsingPost,
queryOperatorByIdUsingGet,
updateOperatorByIdUsingPut,
uploadOperatorChunkUsingPost,
uploadOperatorUsingPost,
} from "../operator.api";
import { sliceFile } from "@/utils/file.util";
export default function OperatorPluginCreate() {
const navigate = useNavigate();
const { id } = useParams();
const { message } = App.useApp();
const [uploadStep, setUploadStep] = useState<
"upload" | "parsing" | "configure" | "preview"
>(id ? "configure" : "upload");
const [isUploading, setIsUploading] = useState(false);
const [parsedInfo, setParsedInfo] = useState({});
const [parseError, setParseError] = useState<string | null>(null);
const { handleUpload, createTask, taskList } = useFileSliceUpload(
{
preUpload: preUploadOperatorUsingPost,
uploadChunk: uploadOperatorChunkUsingPost,
cancelUpload: null,
},
false
);
// 模拟文件上传
const handleFileUpload = async (files: FileList) => {
setIsUploading(true);
setParseError(null);
try {
const fileName = files[0].name;
await handleUpload({
task: createTask({
dataset: { id: "operator-upload", name: "上传算子" },
}),
files: [
{
originFile: files[0],
slices: sliceFile(files[0]),
name: fileName,
size: files[0].size,
},
], // 假设只上传一个文件
});
setParsedInfo({ ...parsedInfo, percent: 100 }); // 上传完成,进度100%
// 解析文件过程
const res = await uploadOperatorUsingPost({ fileName });
const configs = res.data.settings && typeof res.data.settings === "string"
? JSON.parse(res.data.settings)
: {};
const defaultParams: Record<string, string> = {};
Object.keys(configs).forEach((key) => {
const { value } = configs[key];
defaultParams[key] = value;
});
setParsedInfo({ ...res.data, fileName, configs, defaultParams});
setUploadStep("parsing");
} catch (err) {
setParseError("文件解析失败," + err.data.message);
} finally {
setIsUploading(false);
setUploadStep("configure");
}
};
const handlePublish = async () => {
try {
if (id) {
await updateOperatorByIdUsingPut(id, parsedInfo!);
} else {
await createOperatorUsingPost(parsedInfo);
}
setUploadStep("preview");
} catch (err) {
message.error("算子发布失败," + err.data.message);
}
};
const onFetchOperator = async (operatorId: string) => {
// 编辑模式,加载已有算子信息逻辑待实现
const { data } = await queryOperatorByIdUsingGet(operatorId);
const configs = data.settings && typeof data.settings === "string"
? JSON.parse(data.settings)
: {};
const defaultParams: Record<string, string> = {};
Object.keys(configs).forEach((key) => {
const { value } = configs[key];
defaultParams[key] = value;
});
setParsedInfo({ ...data, configs, defaultParams});
setUploadStep("configure");
};
useEffect(() => {
if (id) {
// 编辑模式,加载已有算子信息逻辑待实现
onFetchOperator(id);
}
}, [id]);
return (
<div className="flex-overflow-auto bg-gray-50">
{/* Header */}
<div className="flex justify-between items-center">
<div className="flex items-center gap-4">
<Button type="text" onClick={() => navigate("/data/operator-market")}>
<ArrowLeft className="w-4 h-4" />
</Button>
<h1 className="text-xl font-bold text-gray-900">
{id ? "更新算子" : "上传算子"}
</h1>
</div>
<div className="w-1/2">
<Steps
size="small"
items={[
{
title: "上传文件",
icon: <Upload />,
},
{
title: "解析文件",
icon: <Settings />,
},
{
title: "配置信息",
icon: <TagIcon />,
},
{
title: "发布完成",
icon: <CheckCircle />,
},
]}
current={
uploadStep === "upload"
? 0
: uploadStep === "parsing"
? 1
: uploadStep === "configure"
? 2
: 3
}
/>
</div>
</div>
{/* Content */}
<div className="flex-overflow-auto p-6 mt-4 bg-white border-card">
<div className="flex-overflow-auto">
{uploadStep === "upload" && (
<UploadStep onUpload={handleFileUpload} isUploading={isUploading} />
)}
{uploadStep === "parsing" && (
<ParsingStep
parseProgress={taskList[0]?.percent || parsedInfo.percent || 0}
uploadedFiles={taskList}
/>
)}
{uploadStep === "configure" && (
<ConfigureStep
setParsedInfo={setParsedInfo}
parseError={parseError}
parsedInfo={parsedInfo}
/>
)}
{uploadStep === "preview" && (
<PreviewStep setUploadStep={setUploadStep} />
)}
</div>
{uploadStep === "configure" && (
<div className="flex justify-end gap-3 mt-8">
<Button onClick={() => setUploadStep("upload")}></Button>
<Button type="primary" onClick={handlePublish}>
{id ? "更新" : "发布"}
</Button>
</div>
)}
</div>
</div>
);
}
import { Button, App, Steps } from "antd";
import {
ArrowLeft,
CheckCircle,
Settings,
TagIcon,
Upload,
} from "lucide-react";
import { useNavigate, useParams } from "react-router";
import { useEffect, useState } from "react";
import UploadStep from "./components/UploadStep";
import ParsingStep from "./components/ParsingStep";
import ConfigureStep from "./components/ConfigureStep";
import PreviewStep from "./components/PreviewStep";
import { useFileSliceUpload } from "@/hooks/useSliceUpload";
import {
createOperatorUsingPost,
preUploadOperatorUsingPost,
queryOperatorByIdUsingGet,
updateOperatorByIdUsingPut,
uploadOperatorChunkUsingPost,
uploadOperatorUsingPost,
} from "../operator.api";
import { sliceFile } from "@/utils/file.util";
export default function OperatorPluginCreate() {
const navigate = useNavigate();
const { id } = useParams();
const { message } = App.useApp();
const [uploadStep, setUploadStep] = useState<
"upload" | "parsing" | "configure" | "preview"
>(id ? "configure" : "upload");
const [isUploading, setIsUploading] = useState(false);
const [parsedInfo, setParsedInfo] = useState({});
const [parseError, setParseError] = useState<string | null>(null);
const { handleUpload, createTask, taskList } = useFileSliceUpload(
{
preUpload: preUploadOperatorUsingPost,
uploadChunk: uploadOperatorChunkUsingPost,
cancelUpload: null,
},
false
);
// 模拟文件上传
const handleFileUpload = async (files: FileList) => {
setIsUploading(true);
setParseError(null);
try {
const fileName = files[0].name;
await handleUpload({
task: createTask({
dataset: { id: "operator-upload", name: "上传算子" },
}),
files: [
{
originFile: files[0],
slices: sliceFile(files[0]),
name: fileName,
size: files[0].size,
},
], // 假设只上传一个文件
});
setParsedInfo({ ...parsedInfo, percent: 100 }); // 上传完成,进度100%
// 解析文件过程
const res = await uploadOperatorUsingPost({ fileName });
const configs = res.data.settings && typeof res.data.settings === "string"
? JSON.parse(res.data.settings)
: {};
const defaultParams: Record<string, string> = {};
Object.keys(configs).forEach((key) => {
const { value } = configs[key];
defaultParams[key] = value;
});
setParsedInfo({ ...res.data, fileName, configs, defaultParams});
setUploadStep("parsing");
} catch (err) {
setParseError("文件解析失败," + err.data.message);
} finally {
setIsUploading(false);
setUploadStep("configure");
}
};
const handlePublish = async () => {
try {
if (id) {
await updateOperatorByIdUsingPut(id, parsedInfo!);
} else {
await createOperatorUsingPost(parsedInfo);
}
setUploadStep("preview");
} catch (err) {
message.error("算子发布失败," + err.data.message);
}
};
const onFetchOperator = async (operatorId: string) => {
// 编辑模式,加载已有算子信息逻辑待实现
const { data } = await queryOperatorByIdUsingGet(operatorId);
const configs = data.settings && typeof data.settings === "string"
? JSON.parse(data.settings)
: {};
const defaultParams: Record<string, string> = {};
Object.keys(configs).forEach((key) => {
const { value } = configs[key];
defaultParams[key] = value;
});
setParsedInfo({ ...data, configs, defaultParams});
setUploadStep("configure");
};
useEffect(() => {
if (id) {
// 编辑模式,加载已有算子信息逻辑待实现
onFetchOperator(id);
}
}, [id]);
return (
<div className="flex-overflow-auto bg-gray-50">
{/* Header */}
<div className="flex justify-between items-center">
<div className="flex items-center gap-4">
<Button type="text" onClick={() => navigate("/data/operator-market")}>
<ArrowLeft className="w-4 h-4" />
</Button>
<h1 className="text-xl font-bold text-gray-900">
{id ? "更新算子" : "上传算子"}
</h1>
</div>
<div className="w-1/2">
<Steps
size="small"
items={[
{
title: "上传文件",
icon: <Upload />,
},
{
title: "解析文件",
icon: <Settings />,
},
{
title: "配置信息",
icon: <TagIcon />,
},
{
title: "发布完成",
icon: <CheckCircle />,
},
]}
current={
uploadStep === "upload"
? 0
: uploadStep === "parsing"
? 1
: uploadStep === "configure"
? 2
: 3
}
/>
</div>
</div>
{/* Content */}
<div className="flex-overflow-auto p-6 mt-4 bg-white border-card">
<div className="flex-overflow-auto">
{uploadStep === "upload" && (
<UploadStep onUpload={handleFileUpload} isUploading={isUploading} />
)}
{uploadStep === "parsing" && (
<ParsingStep
parseProgress={taskList[0]?.percent || parsedInfo.percent || 0}
uploadedFiles={taskList}
/>
)}
{uploadStep === "configure" && (
<ConfigureStep
setParsedInfo={setParsedInfo}
parseError={parseError}
parsedInfo={parsedInfo}
/>
)}
{uploadStep === "preview" && (
<PreviewStep setUploadStep={setUploadStep} />
)}
</div>
{uploadStep === "configure" && (
<div className="flex justify-end gap-3 mt-8">
<Button onClick={() => setUploadStep("upload")}></Button>
<Button type="primary" onClick={handlePublish}>
{id ? "更新" : "发布"}
</Button>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,115 +1,115 @@
import { Alert, Input, Form } from "antd";
import TextArea from "antd/es/input/TextArea";
import React, { useEffect } from "react";
import ParamConfig from "@/pages/DataCleansing/Create/components/ParamConfig.tsx";
export default function ConfigureStep({
parsedInfo,
parseError,
setParsedInfo,
}) {
const [form] = Form.useForm();
useEffect(() => {
form.setFieldsValue(parsedInfo);
}, [parsedInfo]);
const handleConfigChange = (
operatorId: string,
paramKey: string,
value: any
) => {
setParsedInfo((op) =>
op.id === operatorId
? {
...op,
overrides: {
...(op?.overrides || op?.defaultParams),
[paramKey]: value,
},
}
: op
);
};
return (
<>
{/* 解析结果 */}
{parseError && (
<div className="mb-4">
<Alert
message="解析过程中发现问题"
description={parseError}
type="error"
showIcon
/>
</div>
)}
{!parseError && parsedInfo && (
<Form
form={form}
layout="vertical"
initialValues={parsedInfo}
onValuesChange={(_, allValues) => {
setParsedInfo({ ...parsedInfo, ...allValues });
}}
>
{/* 基本信息 */}
<h3 className="text-lg font-semibold text-gray-900"></h3>
<Form.Item label="ID" name="id" rules={[{ required: true }]}>
<Input value={parsedInfo.id} readOnly />
</Form.Item>
<Form.Item label="名称" name="name" rules={[{ required: true }]}>
<Input value={parsedInfo.name} />
</Form.Item>
<Form.Item label="版本" name="version" rules={[{ required: true }]}>
<Input value={parsedInfo.version} />
</Form.Item>
<Form.Item
label="描述"
name="description"
rules={[{ required: false }]}
>
<TextArea value={parsedInfo.description} />
</Form.Item>
<Form.Item
label="输入类型"
name="inputs"
rules={[{ required: true }]}
>
<Input value={parsedInfo.inputs} />
</Form.Item>
<Form.Item
label="输出类型"
name="outputs"
rules={[{ required: true }]}
>
<Input value={parsedInfo.outputs} />
</Form.Item>
{parsedInfo.configs && Object.keys(parsedInfo.configs).length > 0 && (
<>
<h3 className="text-lg font-semibold text-gray-900 mt-10 mb-2">
</h3>
<Form layout="vertical">
{Object.entries(parsedInfo?.configs).map(([key, param]) => (
<ParamConfig
key={key}
operator={parsedInfo}
paramKey={key}
param={param}
onParamChange={handleConfigChange}
/>
))}
</Form>
</>
)}
{/* <h3 className="text-lg font-semibold text-gray-900 mt-8">高级配置</h3> */}
</Form>
)}
</>
);
}
import { Alert, Input, Form } from "antd";
import TextArea from "antd/es/input/TextArea";
import React, { useEffect } from "react";
import ParamConfig from "@/pages/DataCleansing/Create/components/ParamConfig.tsx";
export default function ConfigureStep({
parsedInfo,
parseError,
setParsedInfo,
}) {
const [form] = Form.useForm();
useEffect(() => {
form.setFieldsValue(parsedInfo);
}, [parsedInfo]);
const handleConfigChange = (
operatorId: string,
paramKey: string,
value: any
) => {
setParsedInfo((op) =>
op.id === operatorId
? {
...op,
overrides: {
...(op?.overrides || op?.defaultParams),
[paramKey]: value,
},
}
: op
);
};
return (
<>
{/* 解析结果 */}
{parseError && (
<div className="mb-4">
<Alert
message="解析过程中发现问题"
description={parseError}
type="error"
showIcon
/>
</div>
)}
{!parseError && parsedInfo && (
<Form
form={form}
layout="vertical"
initialValues={parsedInfo}
onValuesChange={(_, allValues) => {
setParsedInfo({ ...parsedInfo, ...allValues });
}}
>
{/* 基本信息 */}
<h3 className="text-lg font-semibold text-gray-900"></h3>
<Form.Item label="ID" name="id" rules={[{ required: true }]}>
<Input value={parsedInfo.id} readOnly />
</Form.Item>
<Form.Item label="名称" name="name" rules={[{ required: true }]}>
<Input value={parsedInfo.name} />
</Form.Item>
<Form.Item label="版本" name="version" rules={[{ required: true }]}>
<Input value={parsedInfo.version} />
</Form.Item>
<Form.Item
label="描述"
name="description"
rules={[{ required: false }]}
>
<TextArea value={parsedInfo.description} />
</Form.Item>
<Form.Item
label="输入类型"
name="inputs"
rules={[{ required: true }]}
>
<Input value={parsedInfo.inputs} />
</Form.Item>
<Form.Item
label="输出类型"
name="outputs"
rules={[{ required: true }]}
>
<Input value={parsedInfo.outputs} />
</Form.Item>
{parsedInfo.configs && Object.keys(parsedInfo.configs).length > 0 && (
<>
<h3 className="text-lg font-semibold text-gray-900 mt-10 mb-2">
</h3>
<Form layout="vertical">
{Object.entries(parsedInfo?.configs).map(([key, param]) => (
<ParamConfig
key={key}
operator={parsedInfo}
paramKey={key}
param={param}
onParamChange={handleConfigChange}
/>
))}
</Form>
</>
)}
{/* <h3 className="text-lg font-semibold text-gray-900 mt-8">高级配置</h3> */}
</Form>
)}
</>
);
}

View File

@@ -1,50 +1,50 @@
import { Progress } from "antd";
import { Settings, FileText, CheckCircle } from "lucide-react";
export default function ParsingStep({ parseProgress, uploadedFiles }) {
return (
<div className="text-center py-2">
<div className="w-24 h-24 mx-auto mb-6 bg-blue-50 rounded-full flex items-center justify-center">
<Settings className="w-12 h-12 text-blue-500 animate-spin" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4">
</h2>
<p className="text-gray-600 mb-8">
...
</p>
{/* 已上传文件列表 */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<div className="space-y-2">
{uploadedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-gray-500" />
<span className="font-medium">{file.name}</span>
<span className="text-sm text-gray-500">
({(file.size / 1024).toFixed(1)} KB)
</span>
</div>
<CheckCircle className="w-5 h-5 text-green-500" />
</div>
))}
</div>
</div>
{/* 解析进度 */}
<div className="max-w-md mx-auto">
<Progress
percent={parseProgress}
status="active"
strokeColor="#3B82F6"
/>
<p className="mt-2 text-sm text-gray-600">: {parseProgress}%</p>
</div>
</div>
);
}
import { Progress } from "antd";
import { Settings, FileText, CheckCircle } from "lucide-react";
export default function ParsingStep({ parseProgress, uploadedFiles }) {
return (
<div className="text-center py-2">
<div className="w-24 h-24 mx-auto mb-6 bg-blue-50 rounded-full flex items-center justify-center">
<Settings className="w-12 h-12 text-blue-500 animate-spin" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4">
</h2>
<p className="text-gray-600 mb-8">
...
</p>
{/* 已上传文件列表 */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<div className="space-y-2">
{uploadedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div className="flex items-center gap-3">
<FileText className="w-5 h-5 text-gray-500" />
<span className="font-medium">{file.name}</span>
<span className="text-sm text-gray-500">
({(file.size / 1024).toFixed(1)} KB)
</span>
</div>
<CheckCircle className="w-5 h-5 text-green-500" />
</div>
))}
</div>
</div>
{/* 解析进度 */}
<div className="max-w-md mx-auto">
<Progress
percent={parseProgress}
status="active"
strokeColor="#3B82F6"
/>
<p className="mt-2 text-sm text-gray-600">: {parseProgress}%</p>
</div>
</div>
);
}

View File

@@ -1,26 +1,26 @@
import { Button } from "antd";
import { CheckCircle, Plus } from "lucide-react";
import { useNavigate } from "react-router";
export default function PreviewStep({ setUploadStep }) {
const navigate = useNavigate();
return (
<div className="text-center py-2">
<div className="w-24 h-24 mx-auto mb-6 bg-green-50 rounded-full flex items-center justify-center">
<CheckCircle className="w-12 h-12 text-green-500" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4"></h2>
<p className="text-gray-600 mb-8"></p>
<div className="flex justify-center gap-4">
<Button onClick={() => setUploadStep("upload")}>
<Plus className="w-4 h-4 mr-2" />
</Button>
<Button type="primary" onClick={() => navigate("/data/operator-market")}>
</Button>
</div>
</div>
);
}
import { Button } from "antd";
import { CheckCircle, Plus } from "lucide-react";
import { useNavigate } from "react-router";
export default function PreviewStep({ setUploadStep }) {
const navigate = useNavigate();
return (
<div className="text-center py-2">
<div className="w-24 h-24 mx-auto mb-6 bg-green-50 rounded-full flex items-center justify-center">
<CheckCircle className="w-12 h-12 text-green-500" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4"></h2>
<p className="text-gray-600 mb-8"></p>
<div className="flex justify-center gap-4">
<Button onClick={() => setUploadStep("upload")}>
<Plus className="w-4 h-4 mr-2" />
</Button>
<Button type="primary" onClick={() => navigate("/data/operator-market")}>
</Button>
</div>
</div>
);
}

View File

@@ -1,79 +1,79 @@
import { Spin } from "antd";
import { Upload, FileText } from "lucide-react";
export default function UploadStep({ isUploading, onUpload }) {
const supportedFormats = [
{ ext: ".zip", desc: "压缩包文件" },
{ ext: ".tar", desc: "压缩包文件" },
];
return (
<div className="py-2 w-full text-center">
<div className="w-24 h-24 mx-auto mb-6 bg-blue-50 rounded-full flex items-center justify-center">
<Upload className="w-12 h-12 text-blue-500" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4"></h2>
<p className="text-gray-600 mb-8">
</p>
{/* 支持的格式 */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
</h3>
<div className="flex gap-4">
{supportedFormats.map((format, index) => (
<div key={index} className="p-3 border border-gray-200 rounded-lg flex-1">
<div className="font-medium text-gray-900">{format.ext}</div>
<div className="text-sm text-gray-500">{format.desc}</div>
</div>
))}
</div>
</div>
{/* 文件上传区域 */}
<div
className="border-2 border-dashed border-gray-300 rounded-lg p-8 hover:border-blue-400 transition-colors cursor-pointer"
onDrop={(e) => {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length > 0) {
onUpload(files);
}
}}
onDragOver={(e) => e.preventDefault()}
onClick={() => {
const input = document.createElement("input");
input.type = "file";
input.multiple = false;
input.accept = supportedFormats.map((f) => f.ext).join(",");
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files;
if (files) {
onUpload(files);
}
};
input.click();
}}
>
{isUploading ? (
<div className="flex flex-col items-center">
<Spin size="large" />
<p className="mt-4 text-gray-600">...</p>
</div>
) : (
<div>
<FileText className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-lg text-gray-600 mb-2">
</p>
<p className="text-sm text-gray-500">
</p>
</div>
)}
</div>
</div>
);
}
import { Spin } from "antd";
import { Upload, FileText } from "lucide-react";
export default function UploadStep({ isUploading, onUpload }) {
const supportedFormats = [
{ ext: ".zip", desc: "压缩包文件" },
{ ext: ".tar", desc: "压缩包文件" },
];
return (
<div className="py-2 w-full text-center">
<div className="w-24 h-24 mx-auto mb-6 bg-blue-50 rounded-full flex items-center justify-center">
<Upload className="w-12 h-12 text-blue-500" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4"></h2>
<p className="text-gray-600 mb-8">
</p>
{/* 支持的格式 */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
</h3>
<div className="flex gap-4">
{supportedFormats.map((format, index) => (
<div key={index} className="p-3 border border-gray-200 rounded-lg flex-1">
<div className="font-medium text-gray-900">{format.ext}</div>
<div className="text-sm text-gray-500">{format.desc}</div>
</div>
))}
</div>
</div>
{/* 文件上传区域 */}
<div
className="border-2 border-dashed border-gray-300 rounded-lg p-8 hover:border-blue-400 transition-colors cursor-pointer"
onDrop={(e) => {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length > 0) {
onUpload(files);
}
}}
onDragOver={(e) => e.preventDefault()}
onClick={() => {
const input = document.createElement("input");
input.type = "file";
input.multiple = false;
input.accept = supportedFormats.map((f) => f.ext).join(",");
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files;
if (files) {
onUpload(files);
}
};
input.click();
}}
>
{isUploading ? (
<div className="flex flex-col items-center">
<Spin size="large" />
<p className="mt-4 text-gray-600">...</p>
</div>
) : (
<div>
<FileText className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-lg text-gray-600 mb-2">
</p>
<p className="text-sm text-gray-500">
</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,149 +1,149 @@
import React, { useEffect } from "react";
import { useState } from "react";
import {Card, Breadcrumb, message} from "antd";
import {
DeleteOutlined, StarFilled,
StarOutlined, UploadOutlined,
} from "@ant-design/icons";
import {Clock, GitBranch} from "lucide-react";
import DetailHeader from "@/components/DetailHeader";
import {Link, useNavigate, useParams} from "react-router";
import Overview from "./components/Overview";
import Install from "./components/Install";
import {deleteOperatorByIdUsingDelete, queryOperatorByIdUsingGet, updateOperatorByIdUsingPut} from "../operator.api";
import { OperatorI } from "../operator.model";
import { mapOperator } from "../operator.const";
export default function OperatorPluginDetail() {
const { id } = useParams(); // 获取动态路由参数
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("overview");
const [isStar, setIsStar] = useState(false);
const [operator, setOperator] = useState<OperatorI | null>(null);
const fetchOperator = async () => {
try {
const { data } = await queryOperatorByIdUsingGet(id as unknown as number);
setOperator(mapOperator(data));
setIsStar(data.isStar)
} catch (error) {
setOperator("error");
}
};
useEffect(() => {
fetchOperator();
}, [id]);
if (!operator) {
return <div>Loading...</div>;
}
if (operator === "error") {
return (
<div className="text-red-500">
Failed to load operator details. Please try again later.
</div>
);
}
const handleStar = async () => {
const data = {
id: operator.id,
isStar: !isStar
};
await updateOperatorByIdUsingPut(operator.id, data)
setIsStar(!isStar)
}
const handleDelete = async () => {
await deleteOperatorByIdUsingDelete(operator.id);
navigate("/data/operator-market");
message.success("算子删除成功");
};
// 模拟算子数据
const statistics = [
{
icon: <GitBranch className="text-blue-400 w-4 h-4" />,
label: "",
value: "v" + operator?.version,
},
{
icon: <Clock className="text-blue-400 w-4 h-4" />,
label: "",
value: operator?.updatedAt,
},
];
const operations = [
{
key: "favorite",
label: "收藏",
icon: (isStar ? (
<StarFilled style={{ color: '#f59e0b' }} />
) : (
<StarOutlined />
)
),
onClick: handleStar,
},
{
key: "update",
label: "更新",
icon: <UploadOutlined />,
onClick: () => navigate("/data/operator-market/create/" + operator.id),
},
{
key: "delete",
label: "删除",
danger: true,
confirm: {
title: "确认删除当前算子?",
description: "删除后该算子将无法恢复,请谨慎操作。",
okText: "删除",
cancelText: "取消",
okType: "danger"
},
icon: <DeleteOutlined />,
onClick: handleDelete,
},
];
return (
<div className="min-h-screen flex flex-col gap-4">
{/* Header */}
<Breadcrumb
items={[
{
title: <Link to="/data/operator-market"></Link>,
href: "/data/operator-market",
},
{
title: operator?.name,
},
]}
/>
<DetailHeader
data={operator}
statistics={statistics}
operations={operations}
/>
<Card
tabList={[
{
key: "overview",
label: "概览",
},
]}
activeTabKey={activeTab}
onTabChange={setActiveTab}
>
{activeTab === "overview" && <Overview operator={operator} />}
{activeTab === "service" && <Install operator={operator} />}
</Card>
</div>
);
}
import React, { useEffect } from "react";
import { useState } from "react";
import {Card, Breadcrumb, message} from "antd";
import {
DeleteOutlined, StarFilled,
StarOutlined, UploadOutlined,
} from "@ant-design/icons";
import {Clock, GitBranch} from "lucide-react";
import DetailHeader from "@/components/DetailHeader";
import {Link, useNavigate, useParams} from "react-router";
import Overview from "./components/Overview";
import Install from "./components/Install";
import {deleteOperatorByIdUsingDelete, queryOperatorByIdUsingGet, updateOperatorByIdUsingPut} from "../operator.api";
import { OperatorI } from "../operator.model";
import { mapOperator } from "../operator.const";
export default function OperatorPluginDetail() {
const { id } = useParams(); // 获取动态路由参数
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("overview");
const [isStar, setIsStar] = useState(false);
const [operator, setOperator] = useState<OperatorI | null>(null);
const fetchOperator = async () => {
try {
const { data } = await queryOperatorByIdUsingGet(id as unknown as number);
setOperator(mapOperator(data));
setIsStar(data.isStar)
} catch (error) {
setOperator("error");
}
};
useEffect(() => {
fetchOperator();
}, [id]);
if (!operator) {
return <div>Loading...</div>;
}
if (operator === "error") {
return (
<div className="text-red-500">
Failed to load operator details. Please try again later.
</div>
);
}
const handleStar = async () => {
const data = {
id: operator.id,
isStar: !isStar
};
await updateOperatorByIdUsingPut(operator.id, data)
setIsStar(!isStar)
}
const handleDelete = async () => {
await deleteOperatorByIdUsingDelete(operator.id);
navigate("/data/operator-market");
message.success("算子删除成功");
};
// 模拟算子数据
const statistics = [
{
icon: <GitBranch className="text-blue-400 w-4 h-4" />,
label: "",
value: "v" + operator?.version,
},
{
icon: <Clock className="text-blue-400 w-4 h-4" />,
label: "",
value: operator?.updatedAt,
},
];
const operations = [
{
key: "favorite",
label: "收藏",
icon: (isStar ? (
<StarFilled style={{ color: '#f59e0b' }} />
) : (
<StarOutlined />
)
),
onClick: handleStar,
},
{
key: "update",
label: "更新",
icon: <UploadOutlined />,
onClick: () => navigate("/data/operator-market/create/" + operator.id),
},
{
key: "delete",
label: "删除",
danger: true,
confirm: {
title: "确认删除当前算子?",
description: "删除后该算子将无法恢复,请谨慎操作。",
okText: "删除",
cancelText: "取消",
okType: "danger"
},
icon: <DeleteOutlined />,
onClick: handleDelete,
},
];
return (
<div className="min-h-screen flex flex-col gap-4">
{/* Header */}
<Breadcrumb
items={[
{
title: <Link to="/data/operator-market"></Link>,
href: "/data/operator-market",
},
{
title: operator?.name,
},
]}
/>
<DetailHeader
data={operator}
statistics={statistics}
operations={operations}
/>
<Card
tabList={[
{
key: "overview",
label: "概览",
},
]}
activeTabKey={activeTab}
onTabChange={setActiveTab}
>
{activeTab === "overview" && <Overview operator={operator} />}
{activeTab === "service" && <Install operator={operator} />}
</Card>
</div>
);
}

View File

@@ -1,34 +1,34 @@
import { Card } from "antd";
import { Badge, ChevronRight } from "lucide-react";
export default function ChangeLog({ operator }) {
return (
<div className="flex flex-col gap-4">
{operator.changelog.map((version, index) => (
<Card key={index}>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">
{version.version}
</h3>
<p className="text-sm text-gray-600">{version.date}</p>
</div>
{index === 0 && (
<Badge className="bg-blue-100 text-blue-800 border border-blue-200">
</Badge>
)}
</div>
<ul className="space-y-2">
{version.changes.map((change, changeIndex) => (
<li key={changeIndex} className="flex items-start gap-2">
<ChevronRight className="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
<span className="text-gray-700">{change}</span>
</li>
))}
</ul>
</Card>
))}
</div>
);
}
import { Card } from "antd";
import { Badge, ChevronRight } from "lucide-react";
export default function ChangeLog({ operator }) {
return (
<div className="flex flex-col gap-4">
{operator.changelog.map((version, index) => (
<Card key={index}>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">
{version.version}
</h3>
<p className="text-sm text-gray-600">{version.date}</p>
</div>
{index === 0 && (
<Badge className="bg-blue-100 text-blue-800 border border-blue-200">
</Badge>
)}
</div>
<ul className="space-y-2">
{version.changes.map((change, changeIndex) => (
<li key={changeIndex} className="flex items-start gap-2">
<ChevronRight className="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
<span className="text-gray-700">{change}</span>
</li>
))}
</ul>
</Card>
))}
</div>
);
}

View File

@@ -1,15 +1,15 @@
import { Card } from "antd";
export default function Documentation({ operator }) {
return (
<div className="flex flex-col gap-4">
<Card>
<div className="prose max-w-none">
<div className="whitespace-pre-wrap text-gray-700 leading-relaxed">
{operator.documentation}
</div>
</div>
</Card>
</div>
);
}
import { Card } from "antd";
export default function Documentation({ operator }) {
return (
<div className="flex flex-col gap-4">
<Card>
<div className="prose max-w-none">
<div className="whitespace-pre-wrap text-gray-700 leading-relaxed">
{operator.documentation}
</div>
</div>
</Card>
</div>
);
}

View File

@@ -1,28 +1,28 @@
import { copyToClipboard } from "@/utils/unit";
import { Card, Button } from "antd";
import { Copy } from "lucide-react";
export default function Examples({ operator }) {
return (
<div className="flex flex-col gap-4">
{operator.examples.map((example, index) => (
<Card key={index}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">
{example.title}
</h3>
<Button size="small" onClick={() => copyToClipboard(example.code)}>
<Copy className="w-4 h-4 mr-2" />
</Button>
</div>
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
<pre className="text-sm">
<code>{example.code}</code>
</pre>
</div>
</Card>
))}
</div>
);
}
import { copyToClipboard } from "@/utils/unit";
import { Card, Button } from "antd";
import { Copy } from "lucide-react";
export default function Examples({ operator }) {
return (
<div className="flex flex-col gap-4">
{operator.examples.map((example, index) => (
<Card key={index}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">
{example.title}
</h3>
<Button size="small" onClick={() => copyToClipboard(example.code)}>
<Copy className="w-4 h-4 mr-2" />
</Button>
</div>
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
<pre className="text-sm">
<code>{example.code}</code>
</pre>
</div>
</Card>
))}
</div>
);
}

View File

@@ -1,105 +1,105 @@
import { Card, Button } from "antd";
import { Copy } from "lucide-react";
export default function renderInstallTab({ operator }) {
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
// 这里可以添加提示消息
};
return (
<div className="flex flex-col gap-4">
{/* 安装命令 */}
<Card>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg font-mono text-sm">
<div className="flex items-center justify-between">
<span>{operator.installCommand}</span>
<Button
size="small"
onClick={() => copyToClipboard(operator.installCommand)}
className="ml-2"
>
<Copy className="w-4 h-4" />
</Button>
</div>
</div>
</Card>
{/* 系统要求 */}
<Card>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<div className="space-y-3">
<div className="flex items-center justify-between py-2 border-b border-gray-100">
<span className="font-medium text-gray-700">Python </span>
<span className="text-gray-900">
{operator.systemRequirements.python}
</span>
</div>
<div className="flex items-center justify-between py-2 border-b border-gray-100">
<span className="font-medium text-gray-700"></span>
<span className="text-gray-900">
{operator.systemRequirements.memory}
</span>
</div>
<div className="flex items-center justify-between py-2 border-b border-gray-100">
<span className="font-medium text-gray-700"></span>
<span className="text-gray-900">
{operator.systemRequirements.storage}
</span>
</div>
<div className="flex items-center justify-between py-2">
<span className="font-medium text-gray-700">GPU </span>
<span className="text-gray-900">
{operator.systemRequirements.gpu}
</span>
</div>
</div>
</Card>
{/* 依赖项 */}
<Card>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<div className="space-y-2">
{operator.dependencies.map((dep, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<span className="font-mono text-sm text-gray-900">{dep}</span>
<Button size="small" onClick={() => copyToClipboard(dep)}>
<Copy className="w-3 h-3" />
</Button>
</div>
))}
</div>
</Card>
{/* 快速开始 */}
<Card>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<div className="space-y-4">
<div>
<h4 className="font-medium text-gray-900 mb-2">1. </h4>
<div className="bg-gray-900 text-gray-100 p-3 rounded font-mono text-sm">
{operator.installCommand}
</div>
</div>
<div>
<h4 className="font-medium text-gray-900 mb-2">2. 使</h4>
<div className="bg-gray-900 text-gray-100 p-3 rounded font-mono text-sm">
{`from image_preprocessor import ImagePreprocessor
processor = ImagePreprocessor()
result = processor.process(image)`}
</div>
</div>
<div>
<h4 className="font-medium text-gray-900 mb-2">3. </h4>
<p className="text-gray-600">
</p>
</div>
</div>
</Card>
</div>
);
}
import { Card, Button } from "antd";
import { Copy } from "lucide-react";
export default function renderInstallTab({ operator }) {
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
// 这里可以添加提示消息
};
return (
<div className="flex flex-col gap-4">
{/* 安装命令 */}
<Card>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg font-mono text-sm">
<div className="flex items-center justify-between">
<span>{operator.installCommand}</span>
<Button
size="small"
onClick={() => copyToClipboard(operator.installCommand)}
className="ml-2"
>
<Copy className="w-4 h-4" />
</Button>
</div>
</div>
</Card>
{/* 系统要求 */}
<Card>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<div className="space-y-3">
<div className="flex items-center justify-between py-2 border-b border-gray-100">
<span className="font-medium text-gray-700">Python </span>
<span className="text-gray-900">
{operator.systemRequirements.python}
</span>
</div>
<div className="flex items-center justify-between py-2 border-b border-gray-100">
<span className="font-medium text-gray-700"></span>
<span className="text-gray-900">
{operator.systemRequirements.memory}
</span>
</div>
<div className="flex items-center justify-between py-2 border-b border-gray-100">
<span className="font-medium text-gray-700"></span>
<span className="text-gray-900">
{operator.systemRequirements.storage}
</span>
</div>
<div className="flex items-center justify-between py-2">
<span className="font-medium text-gray-700">GPU </span>
<span className="text-gray-900">
{operator.systemRequirements.gpu}
</span>
</div>
</div>
</Card>
{/* 依赖项 */}
<Card>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<div className="space-y-2">
{operator.dependencies.map((dep, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<span className="font-mono text-sm text-gray-900">{dep}</span>
<Button size="small" onClick={() => copyToClipboard(dep)}>
<Copy className="w-3 h-3" />
</Button>
</div>
))}
</div>
</Card>
{/* 快速开始 */}
<Card>
<h3 className="text-lg font-semibold text-gray-900 mb-4"></h3>
<div className="space-y-4">
<div>
<h4 className="font-medium text-gray-900 mb-2">1. </h4>
<div className="bg-gray-900 text-gray-100 p-3 rounded font-mono text-sm">
{operator.installCommand}
</div>
</div>
<div>
<h4 className="font-medium text-gray-900 mb-2">2. 使</h4>
<div className="bg-gray-900 text-gray-100 p-3 rounded font-mono text-sm">
{`from image_preprocessor import ImagePreprocessor
processor = ImagePreprocessor()
result = processor.process(image)`}
</div>
</div>
<div>
<h4 className="font-medium text-gray-900 mb-2">3. </h4>
<p className="text-gray-600">
</p>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -1,59 +1,59 @@
import {DescriptionsProps, Card, Descriptions, Tag} from "antd";
export default function Overview({ operator }) {
const descriptionItems: DescriptionsProps["items"] = [
{
key: "version",
label: "版本",
children: operator.version,
},
{
key: "category",
label: "分类",
children: (
<div className="flex flex-wrap gap-2">
{operator.categories.map((category, index) => (
<Tag
key={index}
className="px-3 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded-full"
>
{category}
</Tag>
))}
</div>
),
},
{
key: "inputs",
label: "输入类型",
children: operator.inputs,
},
{
key: "createdAt",
label: "创建时间",
children: operator.createdAt,
},
{
key: "outputs",
label: "输出类型",
children: operator.outputs,
},
{
key: "lastModified",
label: "最后修改",
children: operator.updatedAt,
},
];
return (
<div className="flex flex-col gap-4">
{/* 基本信息 */}
<Card>
<Descriptions column={2} title="基本信息" items={descriptionItems} />
</Card>
<Card title="描述" styles={{header: {borderBottom: 'none'}}}>
<p>{operator.description}</p>
</Card>
</div>
);
}
import {DescriptionsProps, Card, Descriptions, Tag} from "antd";
export default function Overview({ operator }) {
const descriptionItems: DescriptionsProps["items"] = [
{
key: "version",
label: "版本",
children: operator.version,
},
{
key: "category",
label: "分类",
children: (
<div className="flex flex-wrap gap-2">
{operator.categories.map((category, index) => (
<Tag
key={index}
className="px-3 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded-full"
>
{category}
</Tag>
))}
</div>
),
},
{
key: "inputs",
label: "输入类型",
children: operator.inputs,
},
{
key: "createdAt",
label: "创建时间",
children: operator.createdAt,
},
{
key: "outputs",
label: "输出类型",
children: operator.outputs,
},
{
key: "lastModified",
label: "最后修改",
children: operator.updatedAt,
},
];
return (
<div className="flex flex-col gap-4">
{/* 基本信息 */}
<Card>
<Descriptions column={2} title="基本信息" items={descriptionItems} />
</Card>
<Card title="描述" styles={{header: {borderBottom: 'none'}}}>
<p>{operator.description}</p>
</Card>
</div>
);
}

View File

@@ -1,88 +1,88 @@
import { Card } from "antd";
import { Star } from "lucide-react";
export default function Reviews({ operator }) {
return (
<div className="flex flex-col gap-4">
{/* 评分统计 */}
<Card>
<div className="flex items-center gap-6">
<div className="text-center">
<div className="text-3xl font-bold text-gray-900">4.7</div>
<div className="flex items-center justify-center gap-1 mt-1">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
className="w-4 h-4 fill-yellow-400 text-yellow-400"
/>
))}
</div>
<div className="text-sm text-gray-600 mt-1">
{operator.reviews.length}
</div>
</div>
<div className="flex-1">
<div className="space-y-2">
{[5, 4, 3, 2, 1].map((rating) => {
const count = operator.reviews.filter(
(r) => r.rating === rating
).length;
const percentage = (count / operator.reviews.length) * 100;
return (
<div key={rating} className="flex items-center gap-2">
<span className="text-sm text-gray-600 w-8">
{rating}
</span>
<div className="flex-1 bg-gray-200 rounded-full h-2">
<div
className="bg-yellow-400 h-2 rounded-full"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="text-sm text-gray-600 w-8">{count}</span>
</div>
);
})}
</div>
</div>
</div>
</Card>
{/* 评价列表 */}
{operator.reviews.map((review) => (
<Card key={review.id}>
<div className="flex items-start gap-4">
<img
src={review.avatar || "/placeholder.svg"}
alt={review.user}
className="w-10 h-10 rounded-full"
/>
<div className="flex-1">
<div className="flex items-center justify-between mb-2">
<div>
<h4 className="font-medium text-gray-900">{review.user}</h4>
<div className="flex items-center gap-2 mt-1">
<div className="flex items-center gap-1">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
className={`w-4 h-4 ${
star <= review.rating
? "fill-yellow-400 text-yellow-400"
: "text-gray-300"
}`}
/>
))}
</div>
<span className="text-sm text-gray-600">{review.date}</span>
</div>
</div>
</div>
<p className="text-gray-700">{review.comment}</p>
</div>
</div>
</Card>
))}
</div>
);
}
import { Card } from "antd";
import { Star } from "lucide-react";
export default function Reviews({ operator }) {
return (
<div className="flex flex-col gap-4">
{/* 评分统计 */}
<Card>
<div className="flex items-center gap-6">
<div className="text-center">
<div className="text-3xl font-bold text-gray-900">4.7</div>
<div className="flex items-center justify-center gap-1 mt-1">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
className="w-4 h-4 fill-yellow-400 text-yellow-400"
/>
))}
</div>
<div className="text-sm text-gray-600 mt-1">
{operator.reviews.length}
</div>
</div>
<div className="flex-1">
<div className="space-y-2">
{[5, 4, 3, 2, 1].map((rating) => {
const count = operator.reviews.filter(
(r) => r.rating === rating
).length;
const percentage = (count / operator.reviews.length) * 100;
return (
<div key={rating} className="flex items-center gap-2">
<span className="text-sm text-gray-600 w-8">
{rating}
</span>
<div className="flex-1 bg-gray-200 rounded-full h-2">
<div
className="bg-yellow-400 h-2 rounded-full"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="text-sm text-gray-600 w-8">{count}</span>
</div>
);
})}
</div>
</div>
</div>
</Card>
{/* 评价列表 */}
{operator.reviews.map((review) => (
<Card key={review.id}>
<div className="flex items-start gap-4">
<img
src={review.avatar || "/placeholder.svg"}
alt={review.user}
className="w-10 h-10 rounded-full"
/>
<div className="flex-1">
<div className="flex items-center justify-between mb-2">
<div>
<h4 className="font-medium text-gray-900">{review.user}</h4>
<div className="flex items-center gap-2 mt-1">
<div className="flex items-center gap-1">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
className={`w-4 h-4 ${
star <= review.rating
? "fill-yellow-400 text-yellow-400"
: "text-gray-300"
}`}
/>
))}
</div>
<span className="text-sm text-gray-600">{review.date}</span>
</div>
</div>
</div>
<p className="text-gray-700">{review.comment}</p>
</div>
</div>
</Card>
))}
</div>
);
}

View File

@@ -1,215 +1,223 @@
import { useEffect, useState } from "react";
import { Button, message } from "antd";
import {
DeleteOutlined,
EditOutlined,
FilterOutlined,
PlusOutlined,
DownloadOutlined
} from "@ant-design/icons";
import { Boxes } from "lucide-react";
import { SearchControls } from "@/components/SearchControls";
import CardView from "@/components/CardView";
import { useNavigate } from "react-router";
import type {
CategoryTreeI,
OperatorI,
} from "@/pages/OperatorMarket/operator.model";
import Filters from "./components/Filters";
import TagManagement from "@/components/business/TagManagement";
import { ListView } from "./components/List";
import useFetchData from "@/hooks/useFetchData";
import {
deleteOperatorByIdUsingDelete,
downloadExampleOperatorUsingGet,
queryCategoryTreeUsingGet,
queryOperatorsUsingPost,
} from "../operator.api";
import { mapOperator } from "../operator.const";
export default function OperatorMarketPage() {
const navigate = useNavigate();
const [viewMode, setViewMode] = useState<"card" | "list">("card");
const [selectedFilters, setSelectedFilters] = useState<
Record<string, string[]>
>({});
const [showFilters, setShowFilters] = useState(true);
const [categoriesTree, setCategoriesTree] = useState<CategoryTreeI[]>([]);
const initCategoriesTree = async () => {
const { data } = await queryCategoryTreeUsingGet({ page: 0, size: 1000 });
setCategoriesTree(data.content || []);
};
useEffect(() => {
initCategoriesTree();
}, []);
const {
tableData,
pagination,
searchParams,
fetchData,
handleFiltersChange,
handleKeywordChange,
} = useFetchData(queryOperatorsUsingPost, mapOperator);
const handleUploadOperator = () => {
navigate(`/data/operator-market/create`);
};
const handleDownload = async () => {
await downloadExampleOperatorUsingGet("test_operator.tar");
message.success("文件下载成功");
};
const handleUpdateOperator = (operator: OperatorI) => {
navigate(`/data/operator-market/create/${operator.id}`);
};
const handleDeleteOperator = async (operator: OperatorI) => {
try {
await deleteOperatorByIdUsingDelete(operator.id);
message.success("算子删除成功");
fetchData();
} catch (error) {
message.error("算子删除失败");
}
};
const operations = [
{
key: "edit",
label: "更新",
icon: <EditOutlined />,
onClick: handleUpdateOperator,
},
{
key: "delete",
label: "删除",
danger: true,
icon: <DeleteOutlined />,
confirm: {
title: "确认删除",
description: "此操作不可撤销,是否继续?",
okText: "删除",
okType: "danger",
cancelText: "取消",
},
onClick: handleDeleteOperator,
},
];
useEffect(() => {
if (Object.keys(selectedFilters).length === 0) {
return;
}
const filteredIds = Object.values(selectedFilters).reduce(
(acc, filter: string[]) => {
if (filter.length) {
acc.push(...filter);
}
return acc;
},
[]
);
fetchData({ categories: filteredIds?.length ? filteredIds : undefined });
}, [selectedFilters]);
return (
<div className="h-full flex flex-col gap-4">
{/* Header */}
<div className="flex justify-between">
<h1 className="text-xl font-bold text-gray-900"></h1>
<div className="flex gap-2">
{/*<TagManagement />*/}
<Button
icon={<DownloadOutlined />}
onClick={handleDownload}
>
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleUploadOperator}
>
</Button>
</div>
</div>
{/* Main Content */}
<div className="flex-overflow-auto flex-row border-card">
<div
className={`border-r border-gray-100 transition-all duration-300 ${
showFilters
? "translate-x-0 w-56"
: "-translate-x-full w-0 opacity-0"
}`}
>
<Filters
hideFilter={() => setShowFilters(false)}
categoriesTree={categoriesTree}
selectedFilters={selectedFilters}
setSelectedFilters={setSelectedFilters}
/>
</div>
<div className="flex-overflow-auto p-6 ">
<div className="flex w-full items-top gap-4 border-b border-gray-200 mb-4">
{!showFilters && (
<Button
type="text"
icon={<FilterOutlined />}
onClick={() => setShowFilters(true)}
/>
)}
<div className="flex-1 mb-4">
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange}
searchPlaceholder="搜索算子名称、描述..."
filters={[]}
onFiltersChange={handleFiltersChange}
viewMode={viewMode}
onViewModeChange={setViewMode}
showViewToggle={true}
onReload={fetchData}
/>
</div>
</div>
{/* Content */}
{tableData.length === 0 ? (
<div className="text-center py-12">
<Boxes className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
</h3>
<p className="text-gray-500"></p>
</div>
) : (
<>
{viewMode === "card" ? (
<CardView
data={tableData}
pagination={pagination}
operations={operations}
onView={(item) => navigate(`/data/operator-market/plugin-detail/${item.id}`)}
/>
) : (
<ListView
operators={tableData}
operations={operations}
pagination={pagination}
/>
)}
</>
)}
</div>
</div>
</div>
);
}
import { useEffect, useState } from "react";
import { Button, message } from "antd";
import {
DeleteOutlined,
EditOutlined,
FilterOutlined,
PlusOutlined,
DownloadOutlined
} from "@ant-design/icons";
import { Boxes } from "lucide-react";
import { SearchControls } from "@/components/SearchControls";
import CardView from "@/components/CardView";
import { useNavigate } from "react-router";
import type {
CategoryTreeI,
OperatorI,
} from "@/pages/OperatorMarket/operator.model";
import Filters from "./components/Filters";
import TagManagement from "@/components/business/TagManagement";
import { ListView } from "./components/List";
import useFetchData from "@/hooks/useFetchData";
import {
deleteOperatorByIdUsingDelete,
downloadExampleOperatorUsingGet,
queryCategoryTreeUsingGet,
queryOperatorsUsingPost,
} from "../operator.api";
import { mapOperator } from "../operator.const";
export default function OperatorMarketPage() {
const navigate = useNavigate();
const [viewMode, setViewMode] = useState<"card" | "list">("card");
const [selectedFilters, setSelectedFilters] = useState<
Record<string, string[]>
>({});
const [showFilters, setShowFilters] = useState(true);
const [categoriesTree, setCategoriesTree] = useState<CategoryTreeI[]>([]);
const initCategoriesTree = async () => {
const { data } = await queryCategoryTreeUsingGet({ page: 0, size: 1000 });
setCategoriesTree(data.content || []);
};
useEffect(() => {
initCategoriesTree();
}, []);
const {
tableData,
pagination,
searchParams,
setSearchParams,
fetchData,
handleFiltersChange,
handleKeywordChange,
} = useFetchData(queryOperatorsUsingPost, mapOperator);
const handleUploadOperator = () => {
navigate(`/data/operator-market/create`);
};
const handleDownload = async () => {
await downloadExampleOperatorUsingGet("test_operator.tar");
message.success("文件下载成功");
};
const handleUpdateOperator = (operator: OperatorI) => {
navigate(`/data/operator-market/create/${operator.id}`);
};
const handleDeleteOperator = async (operator: OperatorI) => {
try {
await deleteOperatorByIdUsingDelete(operator.id);
message.success("算子删除成功");
fetchData();
} catch (error) {
message.error("算子删除失败");
}
};
const operations = [
{
key: "edit",
label: "更新",
icon: <EditOutlined />,
onClick: handleUpdateOperator,
},
{
key: "delete",
label: "删除",
danger: true,
icon: <DeleteOutlined />,
confirm: {
title: "确认删除",
description: "此操作不可撤销,是否继续?",
okText: "删除",
okType: "danger",
cancelText: "取消",
},
onClick: handleDeleteOperator,
},
];
useEffect(() => {
const filteredIds = Object.values(selectedFilters).reduce(
(acc, filter: string[]) => {
if (filter.length) {
acc.push(...filter);
}
return acc;
},
[]
);
// 分类筛选变化时:
// 1. 将分类 ID 写入通用 searchParams.filter.categories,确保分页时条件不会丢失
// 2. 将页码重置为 1,避免从“全选”页的当前页跳入细分列表的同一页
setSearchParams((prev) => ({
...prev,
current: 1,
filter: {
...prev.filter,
categories: filteredIds,
},
}));
}, [selectedFilters, setSearchParams]);
return (
<div className="h-full flex flex-col gap-4">
{/* Header */}
<div className="flex justify-between">
<h1 className="text-xl font-bold text-gray-900"></h1>
<div className="flex gap-2">
{/*<TagManagement />*/}
<Button
icon={<DownloadOutlined />}
onClick={handleDownload}
>
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleUploadOperator}
>
</Button>
</div>
</div>
{/* Main Content */}
<div className="flex-overflow-auto flex-row border-card">
<div
className={`border-r border-gray-100 transition-all duration-300 ${
showFilters
? "translate-x-0 w-56"
: "-translate-x-full w-0 opacity-0"
}`}
>
<Filters
hideFilter={() => setShowFilters(false)}
categoriesTree={categoriesTree}
selectedFilters={selectedFilters}
setSelectedFilters={setSelectedFilters}
/>
</div>
<div className="flex-overflow-auto p-6 ">
<div className="flex w-full items-top gap-4 border-b border-gray-200 mb-4">
{!showFilters && (
<Button
type="text"
icon={<FilterOutlined />}
onClick={() => setShowFilters(true)}
/>
)}
<div className="flex-1 mb-4">
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange}
searchPlaceholder="搜索算子名称、描述..."
filters={[]}
onFiltersChange={handleFiltersChange}
viewMode={viewMode}
onViewModeChange={setViewMode}
showViewToggle={true}
onReload={fetchData}
/>
</div>
</div>
{/* Content */}
{tableData.length === 0 ? (
<div className="text-center py-12">
<Boxes className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
</h3>
<p className="text-gray-500"></p>
</div>
) : (
<>
{viewMode === "card" ? (
<CardView
data={tableData}
pagination={pagination}
operations={operations}
onView={(item) => navigate(`/data/operator-market/plugin-detail/${item.id}`)}
/>
) : (
<ListView
operators={tableData}
operations={operations}
pagination={pagination}
/>
)}
</>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,177 +1,177 @@
import { Button, Checkbox, Tooltip } from "antd";
import { FilterOutlined } from "@ant-design/icons";
import React from "react";
import { CategoryI, CategoryTreeI } from "../../operator.model";
interface FilterOption {
key: string;
label: string;
count: number;
icon?: React.ReactNode;
color?: string;
}
interface FilterSectionProps {
title: string;
total: number;
options: FilterOption[];
selectedValues: string[];
onSelectionChange: (values: string[]) => void;
showIcons?: boolean;
badgeColor?: string;
}
const FilterSection: React.FC<FilterSectionProps> = ({
total,
title,
options,
selectedValues,
onSelectionChange,
showIcons = false,
}) => {
const handleCheckboxChange = (value: string, checked: boolean) => {
if (checked) {
onSelectionChange([...selectedValues, value]);
} else {
onSelectionChange(selectedValues.filter((v) => v !== value));
}
};
// 全选功能
const isAllSelected =
options.length > 0 && selectedValues.length === options.length;
const isIndeterminate =
selectedValues.length > 0 && selectedValues.length < options.length;
const handleSelectAll = (checked: boolean) => {
if (checked) {
// 全选
onSelectionChange(options.map((option) => option.key));
} else {
// 全不选
onSelectionChange([]);
}
};
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-900">{title}</h4>
</div>
<div className="space-y-1 text-sm">
{/* 全选选项 */}
{options.length > 1 && (
<label className="flex items-center space-x-2 cursor-pointer border-b border-gray-100 pb-1 ">
<Checkbox
checked={isAllSelected}
indeterminate={isIndeterminate}
onChange={(e) => handleSelectAll(e.target.checked)}
/>
<div className="flex items-center gap-1 flex-1 ml-1">
<span className="text-gray-600 font-medium"></span>
</div>
<span className="text-gray-400">({total})</span>
</label>
)}
{/* 各个选项 */}
{options.map((option) => (
<label
key={option.key}
className="flex items-center space-x-2 cursor-pointer"
>
<Checkbox
checked={selectedValues.includes(option.key)}
onChange={(e) =>
handleCheckboxChange(option.key, e.target.checked)
}
/>
<div className="flex items-center gap-1 flex-1 ml-1">
{showIcons && option.icon}
<span className={`text-gray-700 ${option.color || ""}`}>
{option.label}
</span>
</div>
<span className="text-gray-400">({option.count})</span>
</label>
))}
</div>
</div>
);
};
interface FiltersProps {
categoriesTree: CategoryTreeI[];
selectedFilters: { [key: string]: string[] };
hideFilter: () => void;
setSelectedFilters: (filters: { [key: string]: string[] }) => void;
}
const Filters: React.FC<FiltersProps> = ({
categoriesTree,
selectedFilters,
hideFilter,
setSelectedFilters,
}) => {
const clearAllFilters = () => {
const newFilters = Object.keys(selectedFilters).reduce((acc, key) => {
acc[key] = [];
return acc;
}, {} as { [key: string]: string[] });
setSelectedFilters(newFilters);
};
const hasActiveFilters = Object.values(selectedFilters).some(
(filters) => Array.isArray(filters) && filters.length > 0
);
return (
<div className="p-6 space-y-4 h-full overflow-y-auto">
{/* Filter Header */}
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-900 flex items-center gap-2">
<Tooltip title="隐藏筛选器">
<Button
type="text"
size="small"
icon={<FilterOutlined />}
onClick={hideFilter}
className="cursor-pointer hover:text-blue-500"
></Button>
</Tooltip>
<span></span>
</h3>
{hasActiveFilters && (
<span
onClick={clearAllFilters}
className="cursor-pointer text-sm text-gray-500 hover:text-blue-500"
>
</span>
)}
</div>
{/* Filter Sections */}
{categoriesTree.map((category: CategoryTreeI) => (
<FilterSection
key={category.id}
total={category.count}
title={category.name}
options={category.categories.map((cat: CategoryI) => ({
key: cat.id.toString(),
label: cat.name,
count: cat.count,
}))}
selectedValues={selectedFilters[category.id] || []}
onSelectionChange={(values) =>
setSelectedFilters({ ...selectedFilters, [category.id]: values })
}
showIcons={false}
/>
))}
</div>
);
};
export default Filters;
import { Button, Checkbox, Tooltip } from "antd";
import { FilterOutlined } from "@ant-design/icons";
import React from "react";
import { CategoryI, CategoryTreeI } from "../../operator.model";
interface FilterOption {
key: string;
label: string;
count: number;
icon?: React.ReactNode;
color?: string;
}
interface FilterSectionProps {
title: string;
total: number;
options: FilterOption[];
selectedValues: string[];
onSelectionChange: (values: string[]) => void;
showIcons?: boolean;
badgeColor?: string;
}
const FilterSection: React.FC<FilterSectionProps> = ({
total,
title,
options,
selectedValues,
onSelectionChange,
showIcons = false,
}) => {
const handleCheckboxChange = (value: string, checked: boolean) => {
if (checked) {
onSelectionChange([...selectedValues, value]);
} else {
onSelectionChange(selectedValues.filter((v) => v !== value));
}
};
// 全选功能
const isAllSelected =
options.length > 0 && selectedValues.length === options.length;
const isIndeterminate =
selectedValues.length > 0 && selectedValues.length < options.length;
const handleSelectAll = (checked: boolean) => {
if (checked) {
// 全选
onSelectionChange(options.map((option) => option.key));
} else {
// 全不选
onSelectionChange([]);
}
};
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-900">{title}</h4>
</div>
<div className="space-y-1 text-sm">
{/* 全选选项 */}
{options.length > 1 && (
<label className="flex items-center space-x-2 cursor-pointer border-b border-gray-100 pb-1 ">
<Checkbox
checked={isAllSelected}
indeterminate={isIndeterminate}
onChange={(e) => handleSelectAll(e.target.checked)}
/>
<div className="flex items-center gap-1 flex-1 ml-1">
<span className="text-gray-600 font-medium"></span>
</div>
<span className="text-gray-400">({total})</span>
</label>
)}
{/* 各个选项 */}
{options.map((option) => (
<label
key={option.key}
className="flex items-center space-x-2 cursor-pointer"
>
<Checkbox
checked={selectedValues.includes(option.key)}
onChange={(e) =>
handleCheckboxChange(option.key, e.target.checked)
}
/>
<div className="flex items-center gap-1 flex-1 ml-1">
{showIcons && option.icon}
<span className={`text-gray-700 ${option.color || ""}`}>
{option.label}
</span>
</div>
<span className="text-gray-400">({option.count})</span>
</label>
))}
</div>
</div>
);
};
interface FiltersProps {
categoriesTree: CategoryTreeI[];
selectedFilters: { [key: string]: string[] };
hideFilter: () => void;
setSelectedFilters: (filters: { [key: string]: string[] }) => void;
}
const Filters: React.FC<FiltersProps> = ({
categoriesTree,
selectedFilters,
hideFilter,
setSelectedFilters,
}) => {
const clearAllFilters = () => {
const newFilters = Object.keys(selectedFilters).reduce((acc, key) => {
acc[key] = [];
return acc;
}, {} as { [key: string]: string[] });
setSelectedFilters(newFilters);
};
const hasActiveFilters = Object.values(selectedFilters).some(
(filters) => Array.isArray(filters) && filters.length > 0
);
return (
<div className="p-6 space-y-4 h-full overflow-y-auto">
{/* Filter Header */}
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-900 flex items-center gap-2">
<Tooltip title="隐藏筛选器">
<Button
type="text"
size="small"
icon={<FilterOutlined />}
onClick={hideFilter}
className="cursor-pointer hover:text-blue-500"
></Button>
</Tooltip>
<span></span>
</h3>
{hasActiveFilters && (
<span
onClick={clearAllFilters}
className="cursor-pointer text-sm text-gray-500 hover:text-blue-500"
>
</span>
)}
</div>
{/* Filter Sections */}
{categoriesTree.map((category: CategoryTreeI) => (
<FilterSection
key={category.id}
total={category.count}
title={category.name}
options={category.categories.map((cat: CategoryI) => ({
key: cat.id.toString(),
label: cat.name,
count: cat.count,
}))}
selectedValues={selectedFilters[category.id] || []}
onSelectionChange={(values) =>
setSelectedFilters({ ...selectedFilters, [category.id]: values })
}
showIcons={false}
/>
))}
</div>
);
};
export default Filters;

View File

@@ -1,90 +1,90 @@
import { Button, List, Tag } from "antd";
import { useNavigate } from "react-router";
import { Operator } from "../../operator.model";
export function ListView({ operators = [], pagination, operations }) {
const navigate = useNavigate();
const handleViewOperator = (operator: Operator) => {
navigate(`/data/operator-market/plugin-detail/${operator.id}`);
};
return (
<List
className="p-4 flex-1 overflow-auto mx-4"
dataSource={operators}
pagination={pagination}
renderItem={(operator) => (
<List.Item
className="hover:bg-gray-50 transition-colors px-6 py-4"
actions={[
// <Button
// key="favorite"
// type="text"
// size="small"
// onClick={() => handleToggleFavorite(operator.id)}
// className={
// favoriteOperators.has(operator.id)
// ? "text-yellow-500 hover:text-yellow-600"
// : "text-gray-400 hover:text-yellow-500"
// }
// icon={
// <StarFilled
// style={{
// fontSize: "16px",
// color: favoriteOperators.has(operator.id)
// ? "#ffcc00ff"
// : "#d1d5db",
// cursor: "pointer",
// }}
// onClick={() => handleToggleFavorite(operator.id)}
// />
// }
// title="收藏"
// />,
...operations.map((operation) => (
<Button
type="text"
size="small"
title={operation.label}
icon={operation.icon}
danger={operation.danger}
onClick={() => operation.onClick(operator)}
/>
)),
]}
>
<List.Item.Meta
avatar={
<div className="w-12 h-12 bg-gradient-to-br from-sky-300 to-blue-500 rounded-lg flex items-center justify-center">
<div className="w-8 h-8 text-white">{operator?.icon}</div>
</div>
}
title={
<div className="flex items-center gap-3">
<span
className="font-medium text-gray-900 cursor-pointer hover:text-blue-600"
onClick={() => handleViewOperator(operator)}
>
{operator.name}
</span>
<Tag color="default">v{operator.version}</Tag>
</div>
}
description={
<div className="space-y-2">
<div className="text-gray-600 ">{operator.description}</div>
{/* <div className="flex items-center gap-4 text-xs text-gray-500">
<span>作者: {operator.author}</span>
<span>类型: {operator.type}</span>
<span>框架: {operator.framework}</span>
<span>使用次数: {operator?.usage?.toLocaleString()}</span>
</div> */}
</div>
}
/>
</List.Item>
)}
/>
);
}
import { Button, List, Tag } from "antd";
import { useNavigate } from "react-router";
import { Operator } from "../../operator.model";
export function ListView({ operators = [], pagination, operations }) {
const navigate = useNavigate();
const handleViewOperator = (operator: Operator) => {
navigate(`/data/operator-market/plugin-detail/${operator.id}`);
};
return (
<List
className="p-4 flex-1 overflow-auto mx-4"
dataSource={operators}
pagination={pagination}
renderItem={(operator) => (
<List.Item
className="hover:bg-gray-50 transition-colors px-6 py-4"
actions={[
// <Button
// key="favorite"
// type="text"
// size="small"
// onClick={() => handleToggleFavorite(operator.id)}
// className={
// favoriteOperators.has(operator.id)
// ? "text-yellow-500 hover:text-yellow-600"
// : "text-gray-400 hover:text-yellow-500"
// }
// icon={
// <StarFilled
// style={{
// fontSize: "16px",
// color: favoriteOperators.has(operator.id)
// ? "#ffcc00ff"
// : "#d1d5db",
// cursor: "pointer",
// }}
// onClick={() => handleToggleFavorite(operator.id)}
// />
// }
// title="收藏"
// />,
...operations.map((operation) => (
<Button
type="text"
size="small"
title={operation.label}
icon={operation.icon}
danger={operation.danger}
onClick={() => operation.onClick(operator)}
/>
)),
]}
>
<List.Item.Meta
avatar={
<div className="w-12 h-12 bg-gradient-to-br from-sky-300 to-blue-500 rounded-lg flex items-center justify-center">
<div className="w-8 h-8 text-white">{operator?.icon}</div>
</div>
}
title={
<div className="flex items-center gap-3">
<span
className="font-medium text-gray-900 cursor-pointer hover:text-blue-600"
onClick={() => handleViewOperator(operator)}
>
{operator.name}
</span>
<Tag color="default">v{operator.version}</Tag>
</div>
}
description={
<div className="space-y-2">
<div className="text-gray-600 ">{operator.description}</div>
{/* <div className="flex items-center gap-4 text-xs text-gray-500">
<span>作者: {operator.author}</span>
<span>类型: {operator.type}</span>
<span>框架: {operator.framework}</span>
<span>使用次数: {operator?.usage?.toLocaleString()}</span>
</div> */}
</div>
}
/>
</List.Item>
)}
/>
);
}

View File

@@ -1,26 +1,26 @@
import { Button } from "antd";
import OperatorUpload from "@/app/(layout)/operator-market/components/operator-upload";
import { ArrowLeft } from "lucide-react";
import { useRouter } from "next/navigation";
export default function OperatorUpdatePage() {
const router = useRouter();
return (
<div className="h-screen bg-gray-50">
{/* Header */}
<div className="flex items-center gap-4">
<Button onClick={() => router.push("/operator-market")} className="flex items-center gap-2">
<ArrowLeft className="w-4 h-4" />
</Button>
<h1 className="text-2xl font-bold text-gray-900"></h1>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
<OperatorUpload />
</div>
</div>
);
import { Button } from "antd";
import OperatorUpload from "@/app/(layout)/operator-market/components/operator-upload";
import { ArrowLeft } from "lucide-react";
import { useRouter } from "next/navigation";
export default function OperatorUpdatePage() {
const router = useRouter();
return (
<div className="h-screen bg-gray-50">
{/* Header */}
<div className="flex items-center gap-4">
<Button onClick={() => router.push("/operator-market")} className="flex items-center gap-2">
<ArrowLeft className="w-4 h-4" />
</Button>
<h1 className="text-2xl font-bold text-gray-900"></h1>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
<OperatorUpload />
</div>
</div>
);
}

View File

@@ -1,95 +1,95 @@
import { get, post, put, del, download } from "@/utils/request";
// 算子列表查询
export function queryOperatorsUsingPost(data: any) {
return post("/api/operators/list", data);
}
// 获取算子分类树
export function queryCategoryTreeUsingGet() {
return get("/api/categories/tree");
}
// 根据ID获取算子详情
export function queryOperatorByIdUsingGet(operatorId: string | number) {
return get(`/api/operators/${operatorId}`);
}
// 创建算子
export function createOperatorUsingPost(data: any) {
return post("/api/operators/create", data);
}
// 更新算子
export function updateOperatorByIdUsingPut(
operatorId: string | number,
data: any
) {
return put(`/api/operators/${operatorId}`, data);
}
// 删除算子
export function deleteOperatorByIdUsingDelete(operatorId: string | number) {
return del(`/api/operators/${operatorId}`);
}
// 上传算子
export function uploadOperatorUsingPost(data: any) {
return post("/api/operators/upload", data);
}
export function preUploadOperatorUsingPost(_, data: any) {
return post("/api/operators/upload/pre-upload", data);
}
export function uploadOperatorChunkUsingPost(_, data: FormData, config?: any) {
return post("/api/operators/upload/chunk", data, {
showLoading: false,
...config,
});
}
export function downloadExampleOperatorUsingGet(fileName: string) {
return download("/api/operators/examples/download", null, fileName);
}
// 发布算子
export function publishOperatorUsingPost(operatorId: string | number) {
return post(`/api/operators/${operatorId}/publish`);
}
// 下架算子
export function unpublishOperatorUsingPost(operatorId: string | number) {
return post(`/api/operators/${operatorId}/unpublish`);
}
// 算子标签管理
export function queryLabelsUsingGet(params?: any) {
return get("/api/labels", params);
}
// 创建算子标签
export function createLabelUsingPost(data: any) {
return post("/api/operators/labels", data);
}
// 更新算子标签
export function updateLabelByIdUsingPut(labelId: string | number, data: any) {
return put(`/api/labels/${labelId}`, data);
}
// 删除算子标签
export function deleteLabelsUsingDelete(labelIds: string[]) {
return del("/api/labels", labelIds);
}
// 创建算子分类
export function createCategoryUsingPost(data: any) {
return post("/api/category", data);
}
// 删除算子分类
export function deleteCategoryUsingDelete(data: { id: string | number }) {
return del("/api/category", data);
}
import { get, post, put, del, download } from "@/utils/request";
// 算子列表查询
export function queryOperatorsUsingPost(data: any) {
return post("/api/operators/list", data);
}
// 获取算子分类树
export function queryCategoryTreeUsingGet() {
return get("/api/categories/tree");
}
// 根据ID获取算子详情
export function queryOperatorByIdUsingGet(operatorId: string | number) {
return get(`/api/operators/${operatorId}`);
}
// 创建算子
export function createOperatorUsingPost(data: any) {
return post("/api/operators/create", data);
}
// 更新算子
export function updateOperatorByIdUsingPut(
operatorId: string | number,
data: any
) {
return put(`/api/operators/${operatorId}`, data);
}
// 删除算子
export function deleteOperatorByIdUsingDelete(operatorId: string | number) {
return del(`/api/operators/${operatorId}`);
}
// 上传算子
export function uploadOperatorUsingPost(data: any) {
return post("/api/operators/upload", data);
}
export function preUploadOperatorUsingPost(_, data: any) {
return post("/api/operators/upload/pre-upload", data);
}
export function uploadOperatorChunkUsingPost(_, data: FormData, config?: any) {
return post("/api/operators/upload/chunk", data, {
showLoading: false,
...config,
});
}
export function downloadExampleOperatorUsingGet(fileName: string) {
return download("/api/operators/examples/download", null, fileName);
}
// 发布算子
export function publishOperatorUsingPost(operatorId: string | number) {
return post(`/api/operators/${operatorId}/publish`);
}
// 下架算子
export function unpublishOperatorUsingPost(operatorId: string | number) {
return post(`/api/operators/${operatorId}/unpublish`);
}
// 算子标签管理
export function queryLabelsUsingGet(params?: any) {
return get("/api/labels", params);
}
// 创建算子标签
export function createLabelUsingPost(data: any) {
return post("/api/operators/labels", data);
}
// 更新算子标签
export function updateLabelByIdUsingPut(labelId: string | number, data: any) {
return put(`/api/labels/${labelId}`, data);
}
// 删除算子标签
export function deleteLabelsUsingDelete(labelIds: string[]) {
return del("/api/labels", labelIds);
}
// 创建算子分类
export function createCategoryUsingPost(data: any) {
return post("/api/category", data);
}
// 删除算子分类
export function deleteCategoryUsingDelete(data: { id: string | number }) {
return del("/api/category", data);
}

View File

@@ -1,12 +1,12 @@
import { Code } from "lucide-react";
import { OperatorI } from "./operator.model";
import {formatDateTime} from "@/utils/unit.ts";
export const mapOperator = (op: OperatorI) => {
return {
...op,
icon: <Code className="w-full h-full" />,
createdAt: formatDateTime(op?.createdAt) || "--",
updatedAt: formatDateTime(op?.updatedAt) || formatDateTime(op?.createdAt) || "--",
};
};
import { Code } from "lucide-react";
import { OperatorI } from "./operator.model";
import {formatDateTime} from "@/utils/unit.ts";
export const mapOperator = (op: OperatorI) => {
return {
...op,
icon: <Code className="w-full h-full" />,
createdAt: formatDateTime(op?.createdAt) || "--",
updatedAt: formatDateTime(op?.updatedAt) || formatDateTime(op?.createdAt) || "--",
};
};

View File

@@ -1,62 +1,62 @@
export interface ConfigI {
type:
| "input"
| "select"
| "radio"
| "checkbox"
| "range"
| "slider"
| "inputNumber"
| "switch"
| "multiple";
value?: number | string | boolean | string[] | number[];
required?: boolean;
description?: string;
key: string;
defaultVal: number | string | boolean | string[];
options?: string[] | { label: string; value: string }[];
min?: number;
max?: number;
step?: number;
properties?: ConfigI[]; // 用于嵌套配置
}
export interface OperatorI {
id: string;
name: string;
type: string;
version: string;
inputs: string;
outputs: string;
icon: React.ReactNode;
description: string;
tags: string[];
isStar?: boolean;
originalId?: string; // 用于标识原始算子ID,便于去重
categories: string[]; // 分类列表
settings: string;
overrides?: { [key: string]: any }; // 用户配置的参数
defaultParams?: { [key: string]: any }; // 默认参数
configs: {
[key: string]: ConfigI;
};
createdAt?: string;
updatedAt?: string;
}
export interface CategoryI {
id: number;
name: string;
count: number; // 该分类下的算子数量
type: string; // e.g., "数据源", "数据清洗", "数据分析", "数据可视化"
parentId?: number; // 父分类ID,若无父分类则为null
value: string;
createdAt: string;
}
export interface CategoryTreeI {
id: string;
name: string;
count: number;
categories: CategoryI[];
}
export interface ConfigI {
type:
| "input"
| "select"
| "radio"
| "checkbox"
| "range"
| "slider"
| "inputNumber"
| "switch"
| "multiple";
value?: number | string | boolean | string[] | number[];
required?: boolean;
description?: string;
key: string;
defaultVal: number | string | boolean | string[];
options?: string[] | { label: string; value: string }[];
min?: number;
max?: number;
step?: number;
properties?: ConfigI[]; // 用于嵌套配置
}
export interface OperatorI {
id: string;
name: string;
type: string;
version: string;
inputs: string;
outputs: string;
icon: React.ReactNode;
description: string;
tags: string[];
isStar?: boolean;
originalId?: string; // 用于标识原始算子ID,便于去重
categories: string[]; // 分类列表
settings: string;
overrides?: { [key: string]: any }; // 用户配置的参数
defaultParams?: { [key: string]: any }; // 默认参数
configs: {
[key: string]: ConfigI;
};
createdAt?: string;
updatedAt?: string;
}
export interface CategoryI {
id: number;
name: string;
count: number; // 该分类下的算子数量
type: string; // e.g., "数据源", "数据清洗", "数据分析", "数据可视化"
parentId?: number; // 父分类ID,若无父分类则为null
value: string;
createdAt: string;
}
export interface CategoryTreeI {
id: string;
name: string;
count: number;
categories: CategoryI[];
}