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