You've already forked DataMate
init datamate
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
import { Button, Steps } from "antd";
|
||||
import {
|
||||
ArrowLeft,
|
||||
CheckCircle,
|
||||
Settings,
|
||||
TagIcon,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import UploadStep from "./components/UploadStep";
|
||||
import ParsingStep from "./components/ParsingStep";
|
||||
import ConfigureStep from "./components/ConfigureStep";
|
||||
import PreviewStep from "./components/PreviewStep";
|
||||
|
||||
interface ParsedOperatorInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
author: string;
|
||||
category: string;
|
||||
modality: string[];
|
||||
type: "preprocessing" | "training" | "inference" | "postprocessing";
|
||||
framework: string;
|
||||
language: string;
|
||||
size: string;
|
||||
dependencies: string[];
|
||||
inputFormat: string[];
|
||||
outputFormat: string[];
|
||||
performance: {
|
||||
accuracy?: number;
|
||||
speed: string;
|
||||
memory: string;
|
||||
};
|
||||
documentation?: string;
|
||||
examples?: string[];
|
||||
}
|
||||
|
||||
export default function OperatorPluginCreate() {
|
||||
const navigate = useNavigate();
|
||||
const [uploadStep, setUploadStep] = useState<
|
||||
"upload" | "parsing" | "configure" | "preview"
|
||||
>("upload");
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
|
||||
const [parseProgress, setParseProgress] = useState(0);
|
||||
const [parsedInfo, setParsedInfo] = useState<ParsedOperatorInfo | null>(null);
|
||||
const [parseError, setParseError] = useState<string | null>(null);
|
||||
// 模拟文件上传
|
||||
const handleFileUpload = useCallback((files: FileList) => {
|
||||
setIsUploading(true);
|
||||
setParseError(null);
|
||||
|
||||
// 模拟文件上传过程
|
||||
setTimeout(() => {
|
||||
const fileArray = Array.from(files).map((file) => ({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
}));
|
||||
setUploadedFiles(fileArray);
|
||||
setIsUploading(false);
|
||||
setUploadStep("parsing");
|
||||
startParsing();
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
// 模拟解析过程
|
||||
const startParsing = useCallback(() => {
|
||||
setParseProgress(0);
|
||||
const interval = setInterval(() => {
|
||||
setParseProgress((prev) => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(interval);
|
||||
// 模拟解析完成
|
||||
setTimeout(() => {
|
||||
setParsedInfo({
|
||||
name: "图像预处理算子",
|
||||
version: "1.2.0",
|
||||
description:
|
||||
"支持图像缩放、裁剪、旋转、颜色空间转换等常用预处理操作,优化了内存使用和处理速度",
|
||||
author: "当前用户",
|
||||
category: "图像处理",
|
||||
modality: ["image"],
|
||||
type: "preprocessing",
|
||||
framework: "PyTorch",
|
||||
language: "Python",
|
||||
size: "2.3MB",
|
||||
dependencies: [
|
||||
"opencv-python>=4.5.0",
|
||||
"pillow>=8.0.0",
|
||||
"numpy>=1.20.0",
|
||||
],
|
||||
inputFormat: ["jpg", "png", "bmp", "tiff"],
|
||||
outputFormat: ["jpg", "png", "tensor"],
|
||||
performance: {
|
||||
accuracy: 99.5,
|
||||
speed: "50ms/image",
|
||||
memory: "128MB",
|
||||
},
|
||||
documentation:
|
||||
"# 图像预处理算子\n\n这是一个高效的图像预处理算子...",
|
||||
examples: [
|
||||
"from operator import ImagePreprocessor\nprocessor = ImagePreprocessor()\nresult = processor.process(image)",
|
||||
],
|
||||
});
|
||||
setUploadStep("configure");
|
||||
}, 500);
|
||||
return 100;
|
||||
}
|
||||
return prev + 10;
|
||||
});
|
||||
}, 200);
|
||||
}, []);
|
||||
|
||||
const handlePublish = () => {
|
||||
// 模拟发布过程
|
||||
setUploadStep("preview");
|
||||
setTimeout(() => {
|
||||
alert("算子发布成功!");
|
||||
// 这里可以重置状态或跳转到其他页面
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col 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">上传算子</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="h-full flex flex-col flex-1 overflow-y-auto p-6 mt-4 bg-white rounded-md shadow">
|
||||
<div className="h-full w-full flex flex-col flex-1 overflow-y-auto">
|
||||
{uploadStep === "upload" && (
|
||||
<UploadStep onUpload={handleFileUpload} isUploading={isUploading} />
|
||||
)}
|
||||
{uploadStep === "parsing" && (
|
||||
<ParsingStep
|
||||
parseProgress={parseProgress}
|
||||
uploadedFiles={uploadedFiles}
|
||||
/>
|
||||
)}
|
||||
{uploadStep === "configure" && (
|
||||
<ConfigureStep
|
||||
setUploadStep={setUploadStep}
|
||||
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 onClick={() => setUploadStep("preview")}>预览</Button>
|
||||
<Button type="primary" onClick={handlePublish}>
|
||||
发布算子
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import { Alert, Input, Button } from "antd";
|
||||
import { CheckCircle, Plus, TagIcon, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function ConfigureStep({ parsedInfo, parseError }) {
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [customTag, setCustomTag] = useState("");
|
||||
const availableTags = [
|
||||
"图像处理",
|
||||
"预处理",
|
||||
"缩放",
|
||||
"裁剪",
|
||||
"旋转",
|
||||
"文本处理",
|
||||
"分词",
|
||||
"中文",
|
||||
"NLP",
|
||||
"医学",
|
||||
"音频处理",
|
||||
"特征提取",
|
||||
"MFCC",
|
||||
"频谱分析",
|
||||
"视频处理",
|
||||
"帧提取",
|
||||
"关键帧",
|
||||
"采样",
|
||||
"多模态",
|
||||
"融合",
|
||||
"深度学习",
|
||||
"注意力机制",
|
||||
"推理加速",
|
||||
"TensorRT",
|
||||
"优化",
|
||||
"GPU",
|
||||
"数据增强",
|
||||
"几何变换",
|
||||
"颜色变换",
|
||||
"噪声",
|
||||
];
|
||||
|
||||
const handleAddCustomTag = () => {
|
||||
if (customTag.trim() && !selectedTags.includes(customTag.trim())) {
|
||||
setSelectedTags([...selectedTags, customTag.trim()]);
|
||||
setCustomTag("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTag = (tagToRemove: string) => {
|
||||
setSelectedTags(selectedTags.filter((tag) => tag !== tagToRemove));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 解析结果 */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<CheckCircle className="w-6 h-6 text-green-500" />
|
||||
<h2 className="text-xl font-bold text-gray-900">解析完成</h2>
|
||||
</div>
|
||||
|
||||
{parseError && (
|
||||
<Alert
|
||||
message="解析过程中发现问题"
|
||||
description={parseError}
|
||||
type="warning"
|
||||
showIcon
|
||||
className="mb-6"
|
||||
/>
|
||||
)}
|
||||
|
||||
{parsedInfo && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* 基本信息 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">基本信息</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
算子名称
|
||||
</label>
|
||||
<div className="p-2 bg-gray-50 rounded border text-gray-900">
|
||||
{parsedInfo.name}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
版本
|
||||
</label>
|
||||
<div className="p-2 bg-gray-50 rounded border text-gray-900">
|
||||
{parsedInfo.version}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
作者
|
||||
</label>
|
||||
<div className="p-2 bg-gray-50 rounded border text-gray-900">
|
||||
{parsedInfo.author}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
分类
|
||||
</label>
|
||||
<div className="p-2 bg-gray-50 rounded border text-gray-900">
|
||||
{parsedInfo.category}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 技术规格 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">技术规格</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
框架
|
||||
</label>
|
||||
<div className="p-2 bg-gray-50 rounded border text-gray-900">
|
||||
{parsedInfo.framework}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
语言
|
||||
</label>
|
||||
<div className="p-2 bg-gray-50 rounded border text-gray-900">
|
||||
{parsedInfo.language}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
类型
|
||||
</label>
|
||||
<div className="p-2 bg-gray-50 rounded border text-gray-900">
|
||||
{parsedInfo.type}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
模态
|
||||
</label>
|
||||
<div className="p-2 bg-gray-50 rounded border text-gray-900">
|
||||
{parsedInfo.modality.join(", ")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 描述 */}
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
描述
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 rounded border text-gray-900">
|
||||
{parsedInfo.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 依赖项 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
依赖项
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 rounded border">
|
||||
<div className="space-y-1">
|
||||
{parsedInfo.dependencies.map((dep, index) => (
|
||||
<div key={index} className="text-sm text-gray-900 font-mono">
|
||||
{dep}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 性能指标 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
性能指标
|
||||
</label>
|
||||
<div className="p-3 bg-gray-50 rounded border space-y-2">
|
||||
{parsedInfo.performance.accuracy && (
|
||||
<div className="text-sm">
|
||||
<span className="font-medium">准确率:</span>{" "}
|
||||
{parsedInfo.performance.accuracy}%
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm">
|
||||
<span className="font-medium">速度:</span>{" "}
|
||||
{parsedInfo.performance.speed}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="font-medium">内存:</span>{" "}
|
||||
{parsedInfo.performance.memory}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 标签配置 */}
|
||||
{/* 预定义标签 */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3">推荐标签</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableTags.map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => {
|
||||
if (selectedTags.includes(tag)) {
|
||||
handleRemoveTag(tag);
|
||||
} else {
|
||||
setSelectedTags([...selectedTags, tag]);
|
||||
}
|
||||
}}
|
||||
className={`px-3 py-1 rounded-full text-sm font-medium border transition-colors ${
|
||||
selectedTags.includes(tag)
|
||||
? "bg-blue-100 text-blue-800 border-blue-200"
|
||||
: "bg-gray-50 text-gray-700 border-gray-200 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 自定义标签 */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3">
|
||||
添加自定义标签
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="输入自定义标签..."
|
||||
value={customTag}
|
||||
onChange={(e) => setCustomTag(e.target.value)}
|
||||
onPressEnter={handleAddCustomTag}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button onClick={handleAddCustomTag} disabled={!customTag.trim()}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 已选标签 */}
|
||||
{selectedTags.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3">
|
||||
已选标签 ({selectedTags.length})
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedTags.map((tag) => (
|
||||
<div
|
||||
key={tag}
|
||||
className="flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium"
|
||||
>
|
||||
<TagIcon className="w-3 h-3" />
|
||||
<span>{tag}</span>
|
||||
<button
|
||||
onClick={() => handleRemoveTag(tag)}
|
||||
className="ml-1 hover:text-blue-600"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Button } from "antd";
|
||||
import { CheckCircle, Plus, Eye } from "lucide-react";
|
||||
|
||||
export default function PreviewStep({ setUploadStep }) {
|
||||
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">
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
查看算子
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Spin } from "antd";
|
||||
import { Upload, FileText } from "lucide-react";
|
||||
|
||||
export default function UploadStep({ isUploading, onUpload }) {
|
||||
const supportedFormats = [
|
||||
{ ext: ".py", desc: "Python 脚本文件" },
|
||||
{ ext: ".zip", desc: "压缩包文件" },
|
||||
{ ext: ".tar.gz", desc: "压缩包文件" },
|
||||
{ ext: ".whl", desc: "Python Wheel 包" },
|
||||
{ ext: ".yaml", desc: "配置文件" },
|
||||
{ ext: ".yml", desc: "配置文件" },
|
||||
{ ext: ".json", desc: "JSON 配置文件" },
|
||||
];
|
||||
|
||||
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="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{supportedFormats.map((format, index) => (
|
||||
<div key={index} className="p-3 border border-gray-200 rounded-lg">
|
||||
<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 = true;
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, Breadcrumb } from "antd";
|
||||
import {
|
||||
FireOutlined,
|
||||
ShareAltOutlined,
|
||||
StarOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Download, Clock, User } from "lucide-react";
|
||||
import DetailHeader from "@/components/DetailHeader";
|
||||
import { Link, useParams } from "react-router";
|
||||
import Overview from "./components/Overview";
|
||||
import Install from "./components/Install";
|
||||
import Documentation from "./components/Documentation";
|
||||
import Examples from "./components/Examples";
|
||||
import ChangeLog from "./components/ChangeLog";
|
||||
import Reviews from "./components/Reviews";
|
||||
import { queryOperatorByIdUsingGet } from "../operator.api";
|
||||
import { OperatorI } from "../operator.model";
|
||||
import { mapOperator } from "../operator.const";
|
||||
|
||||
export default function OperatorPluginDetail() {
|
||||
const { id } = useParams(); // 获取动态路由参数
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const [isFavorited, setIsFavorited] = 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));
|
||||
} 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 mockOperator = {
|
||||
id: 1,
|
||||
name: "图像预处理算子",
|
||||
version: "1.2.0",
|
||||
description:
|
||||
"支持图像缩放、裁剪、旋转、颜色空间转换等常用预处理操作,优化了内存使用和处理速度。这是一个高效、易用的图像预处理工具,适用于各种机器学习和计算机视觉项目。",
|
||||
author: "张三",
|
||||
authorAvatar: "/placeholder-user.jpg",
|
||||
category: "图像处理",
|
||||
modality: ["image"],
|
||||
type: "preprocessing",
|
||||
tags: [
|
||||
"图像处理",
|
||||
"预处理",
|
||||
"缩放",
|
||||
"裁剪",
|
||||
"旋转",
|
||||
"计算机视觉",
|
||||
"深度学习",
|
||||
],
|
||||
createdAt: "2024-01-15",
|
||||
lastModified: "2024-01-23",
|
||||
status: "active",
|
||||
downloads: 1247,
|
||||
usage: 856,
|
||||
stars: 89,
|
||||
framework: "PyTorch",
|
||||
language: "Python",
|
||||
size: "2.3MB",
|
||||
license: "MIT",
|
||||
dependencies: [
|
||||
"opencv-python>=4.5.0",
|
||||
"pillow>=8.0.0",
|
||||
"numpy>=1.20.0",
|
||||
"torch>=1.9.0",
|
||||
"torchvision>=0.10.0",
|
||||
],
|
||||
inputFormat: ["jpg", "png", "bmp", "tiff", "webp"],
|
||||
outputFormat: ["jpg", "png", "tensor", "numpy"],
|
||||
performance: {
|
||||
accuracy: 99.5,
|
||||
speed: "50ms/image",
|
||||
memory: "128MB",
|
||||
throughput: "20 images/sec",
|
||||
},
|
||||
systemRequirements: {
|
||||
python: ">=3.7",
|
||||
memory: ">=2GB RAM",
|
||||
storage: ">=100MB",
|
||||
gpu: "Optional (CUDA support)",
|
||||
},
|
||||
installCommand: "pip install image-preprocessor==1.2.0",
|
||||
documentation: `# 图像预处理算子
|
||||
|
||||
## 概述
|
||||
这是一个高效的图像预处理算子,支持多种常用的图像处理操作。
|
||||
|
||||
## 主要功能
|
||||
- 图像缩放和裁剪
|
||||
- 旋转和翻转
|
||||
- 颜色空间转换
|
||||
- 噪声添加和去除
|
||||
- 批量处理支持
|
||||
|
||||
## 性能特点
|
||||
- 内存优化,支持大图像处理
|
||||
- GPU加速支持
|
||||
- 多线程并行处理
|
||||
- 自动批处理优化`,
|
||||
examples: [
|
||||
{
|
||||
title: "基本使用",
|
||||
code: `from image_preprocessor import ImagePreprocessor
|
||||
|
||||
# 初始化预处理器
|
||||
processor = ImagePreprocessor()
|
||||
|
||||
# 加载图像
|
||||
image = processor.load_image("input.jpg")
|
||||
|
||||
# 执行预处理
|
||||
result = processor.process(
|
||||
image,
|
||||
resize=(224, 224),
|
||||
normalize=True,
|
||||
augment=True
|
||||
)
|
||||
|
||||
# 保存结果
|
||||
processor.save_image(result, "output.jpg")`,
|
||||
},
|
||||
{
|
||||
title: "批量处理",
|
||||
code: `from image_preprocessor import ImagePreprocessor
|
||||
import glob
|
||||
|
||||
processor = ImagePreprocessor()
|
||||
|
||||
# 批量处理图像
|
||||
image_paths = glob.glob("images/*.jpg")
|
||||
results = processor.batch_process(
|
||||
image_paths,
|
||||
resize=(256, 256),
|
||||
crop_center=(224, 224),
|
||||
normalize=True
|
||||
)
|
||||
|
||||
# 保存批量结果
|
||||
for i, result in enumerate(results):
|
||||
processor.save_image(result, f"output_{i}.jpg")`,
|
||||
},
|
||||
{
|
||||
title: "高级配置",
|
||||
code: `from image_preprocessor import ImagePreprocessor, Config
|
||||
|
||||
# 自定义配置
|
||||
config = Config(
|
||||
resize_method="bilinear",
|
||||
color_space="RGB",
|
||||
normalize_mean=[0.485, 0.456, 0.406],
|
||||
normalize_std=[0.229, 0.224, 0.225],
|
||||
augmentation={
|
||||
"rotation": (-15, 15),
|
||||
"brightness": (0.8, 1.2),
|
||||
"contrast": (0.8, 1.2)
|
||||
}
|
||||
)
|
||||
|
||||
processor = ImagePreprocessor(config)
|
||||
result = processor.process(image)`,
|
||||
},
|
||||
],
|
||||
changelog: [
|
||||
{
|
||||
version: "1.2.0",
|
||||
date: "2024-01-23",
|
||||
changes: [
|
||||
"新增批量处理功能",
|
||||
"优化内存使用,减少50%内存占用",
|
||||
"添加GPU加速支持",
|
||||
"修复旋转操作的边界问题",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.1.0",
|
||||
date: "2024-01-10",
|
||||
changes: [
|
||||
"添加颜色空间转换功能",
|
||||
"支持WebP格式",
|
||||
"改进错误处理机制",
|
||||
"更新文档和示例",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.0.0",
|
||||
date: "2024-01-01",
|
||||
changes: [
|
||||
"首次发布",
|
||||
"支持基本图像预处理操作",
|
||||
"包含缩放、裁剪、旋转功能",
|
||||
],
|
||||
},
|
||||
],
|
||||
reviews: [
|
||||
{
|
||||
id: 1,
|
||||
user: "李四",
|
||||
avatar: "/placeholder-user.jpg",
|
||||
rating: 5,
|
||||
date: "2024-01-20",
|
||||
comment:
|
||||
"非常好用的图像预处理工具,性能优秀,文档清晰。在我们的项目中大大提高了数据预处理的效率。",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user: "王五",
|
||||
avatar: "/placeholder-user.jpg",
|
||||
rating: 4,
|
||||
date: "2024-01-18",
|
||||
comment:
|
||||
"功能很全面,但是希望能添加更多的数据增强选项。整体来说是个不错的工具。",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
user: "赵六",
|
||||
avatar: "/placeholder-user.jpg",
|
||||
rating: 5,
|
||||
date: "2024-01-15",
|
||||
comment:
|
||||
"安装简单,使用方便,性能表现超出预期。推荐给所有做图像处理的同学。",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const statistics = [
|
||||
{
|
||||
icon: <Download className="w-4 h-4" />,
|
||||
label: "",
|
||||
value: operator?.downloads?.toLocaleString(),
|
||||
},
|
||||
{
|
||||
icon: <User className="w-4 h-4" />,
|
||||
label: "",
|
||||
value: operator?.author,
|
||||
},
|
||||
{
|
||||
icon: <Clock className="w-4 h-4" />,
|
||||
label: "",
|
||||
value: operator?.lastModified,
|
||||
},
|
||||
];
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "favorite",
|
||||
label: "收藏",
|
||||
icon: (
|
||||
<StarOutlined
|
||||
className={`w-4 h-4 ${
|
||||
isFavorited ? "fill-yellow-400 text-yellow-400" : ""
|
||||
}`}
|
||||
/>
|
||||
),
|
||||
onClick: () => setIsFavorited(!isFavorited),
|
||||
},
|
||||
{
|
||||
key: "share",
|
||||
label: "分享",
|
||||
icon: <ShareAltOutlined />,
|
||||
onClick: () => {
|
||||
/* 分享逻辑 */
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "report",
|
||||
label: "发布",
|
||||
icon: <FireOutlined />,
|
||||
onClick: () => {
|
||||
/* 发布逻辑 */
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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: "概览",
|
||||
},
|
||||
{
|
||||
key: "install",
|
||||
label: "安装",
|
||||
},
|
||||
{
|
||||
key: "documentation",
|
||||
label: "文档",
|
||||
},
|
||||
{
|
||||
key: "examples",
|
||||
label: "示例",
|
||||
},
|
||||
{
|
||||
key: "changelog",
|
||||
label: "更新日志",
|
||||
},
|
||||
{
|
||||
key: "reviews",
|
||||
label: "评价",
|
||||
},
|
||||
]}
|
||||
activeTabKey={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
>
|
||||
{activeTab === "overview" && <Overview operator={operator} />}
|
||||
{activeTab === "install" && <Install operator={operator} />}
|
||||
{activeTab === "documentation" && <Documentation operator={operator} />}
|
||||
{activeTab === "examples" && <Examples operator={operator} />}
|
||||
{activeTab === "changelog" && <ChangeLog operator={operator} />}
|
||||
{activeTab === "reviews" && <Reviews operator={operator} />}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +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>
|
||||
);
|
||||
}
|
||||
105
frontend/src/pages/OperatorMarket/Detail/components/Install.tsx
Normal file
105
frontend/src/pages/OperatorMarket/Detail/components/Install.tsx
Normal file
@@ -0,0 +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>
|
||||
);
|
||||
}
|
||||
167
frontend/src/pages/OperatorMarket/Detail/components/Overview.tsx
Normal file
167
frontend/src/pages/OperatorMarket/Detail/components/Overview.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { DescriptionsProps, Card, Descriptions, Tag } from "antd";
|
||||
import { FileText, ImageIcon, Music, Video } from "lucide-react";
|
||||
|
||||
export default function Overview({ operator }) {
|
||||
const getModalityIcon = (modality: string) => {
|
||||
const iconMap = {
|
||||
text: FileText,
|
||||
image: ImageIcon,
|
||||
audio: Music,
|
||||
video: Video,
|
||||
};
|
||||
const IconComponent = iconMap[modality as keyof typeof iconMap] || FileText;
|
||||
return <IconComponent className="w-4 h-4" />;
|
||||
};
|
||||
|
||||
const descriptionItems: DescriptionsProps["items"] = [
|
||||
{
|
||||
key: "version",
|
||||
label: "版本",
|
||||
children: operator.version,
|
||||
},
|
||||
{
|
||||
key: "category",
|
||||
label: "分类",
|
||||
children: operator.category,
|
||||
},
|
||||
{
|
||||
key: "language",
|
||||
label: "语言",
|
||||
children: operator.language,
|
||||
},
|
||||
{
|
||||
key: "modality",
|
||||
label: "模态",
|
||||
children: (
|
||||
<div className="flex items-center gap-2">
|
||||
{operator.modality.map((mod, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-gray-100 rounded text-sm"
|
||||
>
|
||||
{getModalityIcon(mod)}
|
||||
{mod}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "framework",
|
||||
label: "框架",
|
||||
children: operator.framework,
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
label: "类型",
|
||||
children: operator.type,
|
||||
},
|
||||
{
|
||||
key: "size",
|
||||
label: "大小",
|
||||
children: operator.size,
|
||||
},
|
||||
{
|
||||
key: "license",
|
||||
label: "许可证",
|
||||
children: operator.license,
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "创建时间",
|
||||
children: operator.createdAt,
|
||||
},
|
||||
{
|
||||
key: "lastModified",
|
||||
label: "最后修改",
|
||||
children: operator.lastModified,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* 基本信息 */}
|
||||
<Card>
|
||||
<Descriptions column={2} title="基本信息" items={descriptionItems} />
|
||||
</Card>
|
||||
|
||||
{/* 标签 */}
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">标签</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{operator.tags.map((tag, index) => (
|
||||
<Tag
|
||||
key={index}
|
||||
className="px-3 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 性能指标 */}
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">性能指标</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{operator.performance.accuracy && (
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{operator.performance.accuracy}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">准确率</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{operator.performance.speed}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">处理速度</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{operator.performance.memory}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">内存使用</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{operator.performance.throughput}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">吞吐量</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 输入输出格式 */}
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">支持格式</h3>
|
||||
<Descriptions column={2} bordered size="middle">
|
||||
<Descriptions.Item label="输入格式">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{operator.inputFormat.map((format, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-green-50 text-green-700 border border-green-200 rounded text-sm"
|
||||
>
|
||||
.{format}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="输出格式">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{operator.outputFormat.map((format, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded text-sm"
|
||||
>
|
||||
.{format}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +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>
|
||||
);
|
||||
}
|
||||
181
frontend/src/pages/OperatorMarket/Home/OperatorMarket.tsx
Normal file
181
frontend/src/pages/OperatorMarket/Home/OperatorMarket.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "antd";
|
||||
import { FilterOutlined, PlusOutlined } 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/TagManagement";
|
||||
import { ListView } from "./components/List";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
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 filterOptions = [];
|
||||
|
||||
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,
|
||||
} = useFetchData(queryOperatorsUsingPost, mapOperator);
|
||||
|
||||
const handleViewOperator = (operator: OperatorI) => {
|
||||
navigate(`/data/operator-market/plugin-detail/${operator.id}`);
|
||||
};
|
||||
|
||||
const handleUploadOperator = () => {
|
||||
navigate(`/data/operator-market/create`);
|
||||
};
|
||||
|
||||
const handleUpdateOperator = (operator: OperatorI) => {
|
||||
navigate(`/data/operator-market/edit/${operator.id}`);
|
||||
};
|
||||
|
||||
const handleDeleteTag = (operator: OperatorI) => {
|
||||
// 删除算子逻辑
|
||||
console.log("删除算子", operator);
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "更新算子",
|
||||
onClick: handleUpdateOperator,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除算子",
|
||||
onClick: handleDeleteTag,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(selectedFilters).length === 0) {
|
||||
return;
|
||||
}
|
||||
const filteredIds = Object.values(selectedFilters).reduce(
|
||||
(acc, filter: string[]) => {
|
||||
if (filter.length) {
|
||||
acc.push(...filter.map(Number));
|
||||
}
|
||||
|
||||
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
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleUploadOperator}
|
||||
>
|
||||
上传算子
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-1 overflow-auto h-full bg-white rounded-lg">
|
||||
<div
|
||||
className={`border-r border-gray-200 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-1 bg-yellow flex flex-col px-4 my-4">
|
||||
<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">
|
||||
<SearchControls
|
||||
className="mb-4"
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={(keyword) =>
|
||||
setSearchParams({ ...searchParams, keyword })
|
||||
}
|
||||
searchPlaceholder="搜索算子名称、描述..."
|
||||
filters={filterOptions}
|
||||
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} />
|
||||
) : (
|
||||
<ListView operators={tableData} pagination={pagination} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
179
frontend/src/pages/OperatorMarket/Home/components/Filters.tsx
Normal file
179
frontend/src/pages/OperatorMarket/Home/components/Filters.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
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);
|
||||
};
|
||||
|
||||
console.log(categoriesTree);
|
||||
|
||||
const hasActiveFilters = Object.values(selectedFilters).some(
|
||||
(filters) => Array.isArray(filters) && filters.length > 0
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-4 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>
|
||||
筛选器
|
||||
</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;
|
||||
150
frontend/src/pages/OperatorMarket/Home/components/List.tsx
Normal file
150
frontend/src/pages/OperatorMarket/Home/components/List.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Button, Avatar, List, Tag, Badge } from "antd";
|
||||
import { DeleteOutlined, EditOutlined, StarFilled } from "@ant-design/icons";
|
||||
import { Brain, Code, Cpu, Package, Zap, Settings, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Operator } from "../../operator.model";
|
||||
|
||||
export function ListView({ operators, pagination }) {
|
||||
const navigate = useNavigate();
|
||||
const [favoriteOperators, setFavoriteOperators] = useState<Set<number>>(
|
||||
new Set([1, 3, 6])
|
||||
);
|
||||
const handleUpdateOperator = (operator: Operator) => {
|
||||
navigate(`/data/operator-market/create/${operator.id}`);
|
||||
};
|
||||
const handleViewOperator = (operator: Operator) => {
|
||||
navigate(`/data/operator-market/plugin-detail/${operator.id}`);
|
||||
};
|
||||
const handleToggleFavorite = (operatorId: number) => {
|
||||
setFavoriteOperators((prev) => {
|
||||
const newFavorites = new Set(prev);
|
||||
if (newFavorites.has(operatorId)) {
|
||||
newFavorites.delete(operatorId);
|
||||
} else {
|
||||
newFavorites.add(operatorId);
|
||||
}
|
||||
return newFavorites;
|
||||
});
|
||||
};
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
active: {
|
||||
label: "活跃",
|
||||
color: "green",
|
||||
icon: <Zap className="w-3 h-3" />,
|
||||
},
|
||||
beta: {
|
||||
label: "测试版",
|
||||
color: "blue",
|
||||
icon: <Settings className="w-3 h-3" />,
|
||||
},
|
||||
deprecated: {
|
||||
label: "已弃用",
|
||||
color: "gray",
|
||||
icon: <X className="w-3 h-3" />,
|
||||
},
|
||||
};
|
||||
return (
|
||||
statusConfig[status as keyof typeof statusConfig] || statusConfig.active
|
||||
);
|
||||
};
|
||||
const getTypeIcon = (type: string) => {
|
||||
const iconMap = {
|
||||
preprocessing: Code,
|
||||
training: Brain,
|
||||
inference: Cpu,
|
||||
postprocessing: Package,
|
||||
};
|
||||
const IconComponent = iconMap[type as keyof typeof iconMap] || Code;
|
||||
return <IconComponent className="w-4 h-4" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<List
|
||||
className="p-4 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="edit"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => handleUpdateOperator(operator)}
|
||||
icon={<EditOutlined className="w-4 h-4" />}
|
||||
title="更新算子"
|
||||
/>,
|
||||
<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="收藏"
|
||||
/>,
|
||||
<Button
|
||||
key="delete"
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined className="w-4 h-4" />}
|
||||
title="删除算子"
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-blue-100 to-blue-200 rounded-lg flex items-center justify-center">
|
||||
{operator?.icon}
|
||||
</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>
|
||||
<Badge color={getStatusBadge(operator.status).color}>
|
||||
{getStatusBadge(operator.status).label}
|
||||
</Badge>
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
frontend/src/pages/OperatorMarket/OperatorPluginEdit.tsx
Normal file
26
frontend/src/pages/OperatorMarket/OperatorPluginEdit.tsx
Normal file
@@ -0,0 +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>
|
||||
);
|
||||
}
|
||||
243
frontend/src/pages/OperatorMarket/operator.api.ts
Normal file
243
frontend/src/pages/OperatorMarket/operator.api.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
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 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);
|
||||
}
|
||||
|
||||
// 扩展功能接口(基于常见需求)
|
||||
|
||||
// 收藏/取消收藏算子
|
||||
export function starOperatorUsingPost(operatorId: string | number) {
|
||||
return post(`/api/operators/${operatorId}/star`);
|
||||
}
|
||||
|
||||
// 下载算子
|
||||
export function downloadOperatorUsingGet(operatorId: string | number, filename?: string) {
|
||||
return download(`/api/operators/${operatorId}/download`, null, filename);
|
||||
}
|
||||
|
||||
// 算子评分
|
||||
export function rateOperatorUsingPost(operatorId: string | number, data: { rating: number; comment?: string }) {
|
||||
return post(`/api/operators/${operatorId}/rating`, data);
|
||||
}
|
||||
|
||||
// 获取算子统计信息
|
||||
export function getOperatorStatisticsUsingGet(params?: any) {
|
||||
return get("/api/operators/statistics", params);
|
||||
}
|
||||
|
||||
// 获取我的算子列表
|
||||
export function queryMyOperatorsUsingPost(data: any) {
|
||||
return post("/api/operators/my-operators", data);
|
||||
}
|
||||
|
||||
// 获取收藏的算子列表
|
||||
export function queryFavoriteOperatorsUsingPost(data: any) {
|
||||
return post("/api/operators/favorites", data);
|
||||
}
|
||||
|
||||
// 算子使用统计
|
||||
export function getOperatorUsageStatsUsingGet(operatorId: string | number) {
|
||||
return get(`/api/operators/${operatorId}/usage-stats`);
|
||||
}
|
||||
|
||||
// 算子依赖检查
|
||||
export function checkOperatorDependenciesUsingPost(operatorId: string | number) {
|
||||
return post(`/api/operators/${operatorId}/check-dependencies`);
|
||||
}
|
||||
|
||||
// 算子兼容性检查
|
||||
export function checkOperatorCompatibilityUsingPost(operatorId: string | number, data: any) {
|
||||
return post(`/api/operators/${operatorId}/check-compatibility`, data);
|
||||
}
|
||||
|
||||
// 克隆算子
|
||||
export function cloneOperatorUsingPost(operatorId: string | number, data: any) {
|
||||
return post(`/api/operators/${operatorId}/clone`, data);
|
||||
}
|
||||
|
||||
// 获取算子版本列表
|
||||
export function queryOperatorVersionsUsingGet(operatorId: string | number, params?: any) {
|
||||
return get(`/api/operators/${operatorId}/versions`, params);
|
||||
}
|
||||
|
||||
// 创建算子版本
|
||||
export function createOperatorVersionUsingPost(operatorId: string | number, data: any) {
|
||||
return post(`/api/operators/${operatorId}/versions`, data);
|
||||
}
|
||||
|
||||
// 切换算子版本
|
||||
export function switchOperatorVersionUsingPut(operatorId: string | number, versionId: string | number) {
|
||||
return put(`/api/operators/${operatorId}/versions/${versionId}/switch`);
|
||||
}
|
||||
|
||||
// 删除算子版本
|
||||
export function deleteOperatorVersionUsingDelete(operatorId: string | number, versionId: string | number) {
|
||||
return del(`/api/operators/${operatorId}/versions/${versionId}`);
|
||||
}
|
||||
|
||||
// 算子测试
|
||||
export function testOperatorUsingPost(operatorId: string | number, data: any) {
|
||||
return post(`/api/operators/${operatorId}/test`, data);
|
||||
}
|
||||
|
||||
// 获取算子测试结果
|
||||
export function getOperatorTestResultUsingGet(operatorId: string | number, testId: string | number) {
|
||||
return get(`/api/operators/${operatorId}/test/${testId}/result`);
|
||||
}
|
||||
|
||||
// 算子审核相关
|
||||
export function submitOperatorForReviewUsingPost(operatorId: string | number) {
|
||||
return post(`/api/operators/${operatorId}/submit-review`);
|
||||
}
|
||||
|
||||
export function approveOperatorUsingPost(operatorId: string | number, data?: any) {
|
||||
return post(`/api/operators/${operatorId}/approve`, data);
|
||||
}
|
||||
|
||||
export function rejectOperatorUsingPost(operatorId: string | number, data: { reason: string }) {
|
||||
return post(`/api/operators/${operatorId}/reject`, data);
|
||||
}
|
||||
|
||||
// 获取算子评论列表
|
||||
export function queryOperatorCommentsUsingGet(operatorId: string | number, params?: any) {
|
||||
return get(`/api/operators/${operatorId}/comments`, params);
|
||||
}
|
||||
|
||||
// 添加算子评论
|
||||
export function addOperatorCommentUsingPost(operatorId: string | number, data: any) {
|
||||
return post(`/api/operators/${operatorId}/comments`, data);
|
||||
}
|
||||
|
||||
// 删除算子评论
|
||||
export function deleteOperatorCommentUsingDelete(operatorId: string | number, commentId: string | number) {
|
||||
return del(`/api/operators/${operatorId}/comments/${commentId}`);
|
||||
}
|
||||
|
||||
// 搜索算子
|
||||
export function searchOperatorsUsingPost(data: any) {
|
||||
return post("/api/operators/search", data);
|
||||
}
|
||||
|
||||
// 获取热门算子
|
||||
export function queryPopularOperatorsUsingGet(params?: any) {
|
||||
return get("/api/operators/popular", params);
|
||||
}
|
||||
|
||||
// 获取推荐算子
|
||||
export function queryRecommendedOperatorsUsingGet(params?: any) {
|
||||
return get("/api/operators/recommended", params);
|
||||
}
|
||||
|
||||
// 获取最新算子
|
||||
export function queryLatestOperatorsUsingGet(params?: any) {
|
||||
return get("/api/operators/latest", params);
|
||||
}
|
||||
|
||||
// 算子使用示例
|
||||
export function getOperatorExamplesUsingGet(operatorId: string | number) {
|
||||
return get(`/api/operators/${operatorId}/examples`);
|
||||
}
|
||||
|
||||
// 创建算子使用示例
|
||||
export function createOperatorExampleUsingPost(operatorId: string | number, data: any) {
|
||||
return post(`/api/operators/${operatorId}/examples`, data);
|
||||
}
|
||||
|
||||
// 算子文档
|
||||
export function getOperatorDocumentationUsingGet(operatorId: string | number) {
|
||||
return get(`/api/operators/${operatorId}/documentation`);
|
||||
}
|
||||
|
||||
// 更新算子文档
|
||||
export function updateOperatorDocumentationUsingPut(operatorId: string | number, data: any) {
|
||||
return put(`/api/operators/${operatorId}/documentation`, data);
|
||||
}
|
||||
|
||||
// 批量操作
|
||||
export function batchDeleteOperatorsUsingPost(data: { operatorIds: string[] }) {
|
||||
return post("/api/operators/batch-delete", data);
|
||||
}
|
||||
|
||||
export function batchUpdateOperatorsUsingPost(data: any) {
|
||||
return post("/api/operators/batch-update", data);
|
||||
}
|
||||
|
||||
export function batchPublishOperatorsUsingPost(data: { operatorIds: string[] }) {
|
||||
return post("/api/operators/batch-publish", data);
|
||||
}
|
||||
|
||||
export function batchUnpublishOperatorsUsingPost(data: { operatorIds: string[] }) {
|
||||
return post("/api/operators/batch-unpublish", data);
|
||||
}
|
||||
9
frontend/src/pages/OperatorMarket/operator.const.tsx
Normal file
9
frontend/src/pages/OperatorMarket/operator.const.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Boxes } from "lucide-react";
|
||||
import { OperatorI } from "./operator.model";
|
||||
|
||||
export const mapOperator = (op: OperatorI) => {
|
||||
return {
|
||||
...op,
|
||||
icon: <Boxes className="w-5 h-5 text-gray-500" />,
|
||||
};
|
||||
};
|
||||
55
frontend/src/pages/OperatorMarket/operator.model.ts
Normal file
55
frontend/src/pages/OperatorMarket/operator.model.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
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;
|
||||
icon: React.ReactNode;
|
||||
description: string;
|
||||
tags: string[];
|
||||
isStar?: boolean;
|
||||
originalId?: string; // 用于标识原始算子ID,便于去重
|
||||
categories: number[]; // 分类列表
|
||||
settings: string;
|
||||
overrides?: { [key: string]: any }; // 用户配置的参数
|
||||
defaultParams?: { [key: string]: any }; // 默认参数
|
||||
configs: {
|
||||
[key: string]: ConfigI;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CategoryI {
|
||||
id: number;
|
||||
name: string;
|
||||
count: number; // 该分类下的算子数量
|
||||
type: string; // e.g., "数据源", "数据清洗", "数据分析", "数据可视化"
|
||||
parentId?: number; // 父分类ID,若无父分类则为null
|
||||
}
|
||||
|
||||
export interface CategoryTreeI {
|
||||
id: string;
|
||||
name: string;
|
||||
count: number;
|
||||
categories: CategoryI[];
|
||||
}
|
||||
Reference in New Issue
Block a user