feature: 增加算子详情页;优化算子上传更新逻辑 (#64)

* feature: 增加算子详情页;优化算子上传更新逻辑
This commit is contained in:
hhhhsc701
2025-11-07 16:54:00 +08:00
committed by GitHub
parent 78f50ea520
commit 2138ba23c7
24 changed files with 338 additions and 456 deletions

View File

@@ -112,7 +112,11 @@ function DetailHeader<T>({
key={op.key}
{...op.confirm}
onConfirm={() => {
op?.confirm?.onConfirm?.();
if (op.onClick) {
op.onClick()
} else {
op?.confirm?.onConfirm?.();
}
}}
okType={op.danger ? "danger" : "primary"}
overlayStyle={{ zIndex: 9999 }}

View File

@@ -227,7 +227,7 @@ const ParamConfig: React.FC<ParamConfigProps> = ({
return (
<div className="pl-4 border-l border-gray-300">
{param.properties.map((subParam) => (
<Config
<ParamConfig
key={subParam.key}
operator={operator}
paramKey={subParam.key}

View File

@@ -65,7 +65,15 @@ export default function OperatorPluginCreate() {
setParsedInfo({ ...parsedInfo, percent: 100 }); // 上传完成,进度100%
// 解析文件过程
const res = await uploadOperatorUsingPost({ fileName });
setParsedInfo({ ...parsedInfo, ...res.data, 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);
@@ -91,7 +99,15 @@ export default function OperatorPluginCreate() {
const onFetchOperator = async (operatorId: string) => {
// 编辑模式,加载已有算子信息逻辑待实现
const { data } = await queryOperatorByIdUsingGet(operatorId);
setParsedInfo(data);
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");
};
@@ -127,7 +143,7 @@ export default function OperatorPluginCreate() {
icon: <Settings />,
},
{
title: "配置标签",
title: "配置信息",
icon: <TagIcon />,
},
{

View File

@@ -1,6 +1,7 @@
import { Alert, Input, Form } from "antd";
import {Alert, Input, Form} from "antd";
import TextArea from "antd/es/input/TextArea";
import { useEffect } from "react";
import React, {useEffect} from "react";
import ParamConfig from "@/pages/DataCleansing/Create/components/ParamConfig.tsx";
export default function ConfigureStep({
parsedInfo,
@@ -13,6 +14,24 @@ export default function ConfigureStep({
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 (
<>
{/* 解析结果 */}
@@ -33,50 +52,54 @@ export default function ConfigureStep({
layout="vertical"
initialValues={parsedInfo}
onValuesChange={(_, allValues) => {
setParsedInfo({ ...parsedInfo, ...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 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 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 label="版本" name="version" rules={[{required: true}]}>
<Input value={parsedInfo.version}/>
</Form.Item>
<Form.Item
label="描述"
name="description"
rules={[{ required: false }]}
rules={[{required: false}]}
>
<TextArea value={parsedInfo.description} />
<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>
<h3 className="text-lg font-semibold text-gray-900 mt-10 mb-2">
</h3>
<div className="border p-4 rounded-lg flex items-center justify-between gap-4">
<div className="flex-1">
<span className="bg-[#2196f3] border-radius px-4 py-1 rounded-tl-lg rounded-br-lg text-white">
</span>
<pre className="p-4 text-sm overflow-auto">
{parsedInfo.inputs}
</pre>
</div>
<h1 className="text-3xl">VS</h1>
<div className="flex-1">
<span className="bg-[#4caf50] border-radius px-4 py-1 rounded-tl-lg rounded-br-lg text-white">
</span>
<pre className=" p-4 text-sm overflow-auto">
{parsedInfo.outputs}
</pre>
</div>
</div>
{parsedInfo.configs && (
<>
<h3 className="text-lg font-semibold text-gray-900 mt-10 mb-2">
</h3>
<div className="border p-4 rounded-lg grid grid-cols-2 gap-4">
<Form layout="vertical">
{Object.entries(parsedInfo?.configs).map(([key, param]) =>
<ParamConfig
key={key}
operator={parsedInfo}
paramKey={key}
param={param}
onParamChange={handleConfigChange}
/>
)}
</Form>
</div>
</>
)}
{/* <h3 className="text-lg font-semibold text-gray-900 mt-8">高级配置</h3> */}
</Form>

View File

@@ -3,14 +3,8 @@ 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: ".tar", desc: "压缩包文件" },
{ ext: ".whl", desc: "Python Wheel 包" },
{ ext: ".yaml", desc: "配置文件" },
{ ext: ".yml", desc: "配置文件" },
{ ext: ".json", desc: "JSON 配置文件" },
];
return (
@@ -28,9 +22,9 @@ export default function UploadStep({ isUploading, onUpload }) {
<h3 className="text-lg font-semibold text-gray-900 mb-4">
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="flex gap-4">
{supportedFormats.map((format, index) => (
<div key={index} className="p-3 border border-gray-200 rounded-lg">
<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>
@@ -52,7 +46,7 @@ export default function UploadStep({ isUploading, onUpload }) {
onClick={() => {
const input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.multiple = false;
input.accept = supportedFormats.map((f) => f.ext).join(",");
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files;
@@ -75,7 +69,7 @@ export default function UploadStep({ isUploading, onUpload }) {
</p>
<p className="text-sm text-gray-500">
</p>
</div>
)}

View File

@@ -1,35 +1,33 @@
import React, { useEffect } from "react";
import { useState } from "react";
import { Card, Breadcrumb } from "antd";
import {Card, Breadcrumb, message} from "antd";
import {
FireOutlined,
ShareAltOutlined,
StarOutlined,
DeleteOutlined, StarFilled,
StarOutlined, UploadOutlined,
} from "@ant-design/icons";
import { Download, Clock, User } from "lucide-react";
import {Clock, GitBranch} from "lucide-react";
import DetailHeader from "@/components/DetailHeader";
import { Link, useParams } from "react-router";
import {Link, useNavigate, 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 {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 [isFavorited, setIsFavorited] = useState(false);
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");
}
@@ -51,216 +49,32 @@ export default function OperatorPluginDetail() {
);
}
// 模拟算子数据
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: `# 图像预处理算子
const handleStar = async () => {
const data = {
id: operator.id,
isStar: !isStar
};
await updateOperatorByIdUsingPut(operator.id, data)
setIsStar(!isStar)
}
## 概述
这是一个高效的图像预处理算子,支持多种常用的图像处理操作。
## 主要功能
- 图像缩放和裁剪
- 旋转和翻转
- 颜色空间转换
- 噪声添加和去除
- 批量处理支持
## 性能特点
- 内存优化,支持大图像处理
- 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 handleDelete = async () => {
await deleteOperatorByIdUsingDelete(operator.id);
navigate("/data/operator-market");
message.success("算子删除成功");
};
// 模拟算子数据
const statistics = [
{
icon: <Download className="w-4 h-4" />,
icon: <GitBranch className="text-blue-400 w-4 h-4" />,
label: "",
value: operator?.downloads?.toLocaleString(),
value: "v" + operator?.version,
},
{
icon: <User className="w-4 h-4" />,
icon: <Clock className="text-blue-400 w-4 h-4" />,
label: "",
value: operator?.author,
},
{
icon: <Clock className="w-4 h-4" />,
label: "",
value: operator?.lastModified,
value: operator?.updatedAt,
},
];
@@ -268,30 +82,33 @@ result = processor.process(image)`,
{
key: "favorite",
label: "收藏",
icon: (
<StarOutlined
className={`w-4 h-4 ${
isFavorited ? "fill-yellow-400 text-yellow-400" : ""
}`}
/>
icon: (isStar ? (
<StarFilled style={{ color: '#f59e0b' }} />
) : (
<StarOutlined />
)
),
onClick: () => setIsFavorited(!isFavorited),
onClick: handleStar,
},
{
key: "share",
label: "分享",
icon: <ShareAltOutlined />,
onClick: () => {
/* 分享逻辑 */
},
key: "update",
label: "更新",
icon: <UploadOutlined />,
onClick: () => navigate("/data/operator-market/create/" + operator.id),
},
{
key: "report",
label: "发布",
icon: <FireOutlined />,
onClick: () => {
/* 发布逻辑 */
key: "delete",
label: "删除",
danger: true,
confirm: {
title: "确认删除当前算子?",
description: "删除后该算子将无法恢复,请谨慎操作。",
okText: "删除",
cancelText: "取消",
okType: "danger"
},
icon: <DeleteOutlined />,
onClick: handleDelete,
},
];
@@ -320,36 +137,12 @@ result = processor.process(image)`,
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} />}
{activeTab === "service" && <Install operator={operator} />}
</Card>
</div>
);

View File

@@ -1,18 +1,6 @@
import { DescriptionsProps, Card, Descriptions, Tag } from "antd";
import { FileText, ImageIcon, Music, Video } from "lucide-react";
import {DescriptionsProps, Card, Descriptions, Tag} from "antd";
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",
@@ -22,59 +10,38 @@ export default function Overview({ operator }) {
{
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
<div className="flex flex-wrap gap-2">
{operator.categories.map((category, index) => (
<Tag
key={index}
className="flex items-center gap-1 px-2 py-1 bg-gray-100 rounded text-sm"
className="px-3 py-1 bg-blue-50 text-blue-700 border border-blue-200 rounded-full"
>
{getModalityIcon(mod)}
{mod}
</span>
{category}
</Tag>
))}
</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: "inputs",
label: "输入类型",
children: operator.inputs,
},
{
key: "createdAt",
label: "创建时间",
children: operator.createdAt,
},
{
key: "outputs",
label: "输出类型",
children: operator.outputs,
},
{
key: "lastModified",
label: "最后修改",
children: operator.lastModified,
children: operator.updatedAt,
},
];
return (
@@ -84,83 +51,8 @@ export default function Overview({ operator }) {
<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 title="描述" styles={{header: {borderBottom: 'none'}}}>
<p>{operator.description}</p>
</Card>
</div>
);

View File

@@ -6,7 +6,7 @@ import {
FilterOutlined,
PlusOutlined,
} from "@ant-design/icons";
import { Boxes, Edit } from "lucide-react";
import { Boxes } from "lucide-react";
import { SearchControls } from "@/components/SearchControls";
import CardView from "@/components/CardView";
import { useNavigate } from "react-router";
@@ -186,6 +186,7 @@ export default function OperatorMarketPage() {
data={tableData}
pagination={pagination}
operations={operations}
onView={(item) => navigate(`/data/operator-market/plugin-detail/${item.id}`)}
/>
) : (
<ListView

View File

@@ -1,9 +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) || "--",
};
};

View File

@@ -25,6 +25,9 @@ export interface OperatorI {
id: string;
name: string;
type: string;
version: string;
inputs: string;
outputs: string;
icon: React.ReactNode;
description: string;
tags: string[];
@@ -37,6 +40,8 @@ export interface OperatorI {
configs: {
[key: string]: ConfigI;
};
createdAt?: string;
updatedAt?: string;
}
export interface CategoryI {