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

@@ -1,8 +1,10 @@
package com.datamate.operator.application;
import com.datamate.operator.domain.contants.OperatorConstant;
import com.datamate.operator.domain.repository.CategoryRelationRepository;
import com.datamate.operator.domain.repository.CategoryRepository;
import com.datamate.operator.domain.repository.OperatorRepository;
import com.datamate.operator.interfaces.dto.CategoryDto;
import com.datamate.operator.interfaces.dto.CategoryRelationDto;
import com.datamate.operator.interfaces.dto.CategoryTreeResponse;
@@ -11,9 +13,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -21,6 +21,8 @@ import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class CategoryService {
private final OperatorRepository operatorRepo;
private final CategoryRepository categoryRepo;
private final CategoryRelationRepository categoryRelationRepo;
@@ -40,7 +42,7 @@ public class CategoryService {
.filter(relation -> !StringUtils.equals(relation.getParentId(), "0"))
.collect(Collectors.groupingBy(CategoryDto::getParentId));
return groupedByParentId.entrySet().stream()
List<CategoryTreeResponse> categoryTreeResponses = groupedByParentId.entrySet().stream()
.sorted(categoryComparator(nameMap))
.map(entry -> {
String parentId = entry.getKey();
@@ -55,7 +57,11 @@ public class CategoryService {
}).sorted(Comparator.comparing(CategoryDto::getCreatedAt)).toList());
response.setCount(totalCount.get());
return response;
}).toList();
}).collect(Collectors.toCollection(ArrayList::new));
int stars = operatorRepo.countOperatorByStar(true);
categoryTreeResponses.add(buildStarCategoryTree(stars));
return categoryTreeResponses;
}
private Comparator<Map.Entry<String, List<CategoryDto>>> categoryComparator(Map<String, CategoryDto> categoryMap) {
@@ -65,4 +71,21 @@ public class CategoryService {
return index1.compareTo(index2);
};
}
private CategoryTreeResponse buildStarCategoryTree(int stars) {
CategoryTreeResponse starResponse = new CategoryTreeResponse();
starResponse.setName("收藏状态");
starResponse.setCount(stars);
starResponse.setId("257b27e0-bba9-11f0-89d7-00155d0a6153");
CategoryDto star = new CategoryDto();
star.setId(OperatorConstant.CATEGORY_STAR_ID);
star.setName("已收藏");
star.setValue("isStar");
star.setCount(stars);
star.setParentId("257b27e0-bba9-11f0-89d7-00155d0a6153");
star.setCreatedAt(LocalDateTime.now());
star.setType("predefined");
starResponse.setCategories(Collections.singletonList(star));
return starResponse;
}
}

View File

@@ -2,24 +2,35 @@ package com.datamate.operator.application;
import com.datamate.common.domain.model.ChunkUploadPreRequest;
import com.datamate.common.domain.service.FileService;
import com.datamate.common.infrastructure.exception.BusinessException;
import com.datamate.operator.domain.contants.OperatorConstant;
import com.datamate.operator.infrastructure.converter.OperatorConverter;
import com.datamate.operator.domain.model.OperatorView;
import com.datamate.operator.domain.repository.CategoryRelationRepository;
import com.datamate.operator.domain.repository.OperatorRepository;
import com.datamate.operator.domain.repository.OperatorViewRepository;
import com.datamate.operator.infrastructure.exception.OperatorErrorCode;
import com.datamate.operator.infrastructure.parser.ParserHolder;
import com.datamate.operator.interfaces.dto.OperatorDto;
import com.datamate.operator.interfaces.dto.UploadOperatorRequest;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
@RequiredArgsConstructor
public class OperatorService {
private final OperatorRepository operatorRepo;
@@ -32,6 +43,8 @@ public class OperatorService {
private final FileService fileService;
private final ObjectMapper objectMapper = new ObjectMapper();
@Value("${operator.base.path:/operators}")
private String operatorBasePath;
@@ -53,19 +66,25 @@ public class OperatorService {
@Transactional
public OperatorDto createOperator(OperatorDto req) {
overrideSettings(req);
operatorRepo.insertOperator(req);
relationRepo.batchInsert(req.getId(), req.getCategories());
parserHolder.extractTo(getFileType(req.getFileName()), getUploadPath(req.getFileName()),
getExtractPath(getFileNameWithoutExtension(req.getFileName())));
getExtractPath(getFileNameWithoutExtension(req.getFileName())));
return getOperatorById(req.getId());
}
@Transactional
public OperatorDto updateOperator(String id, OperatorDto req) {
overrideSettings(req);
operatorRepo.updateOperator(req);
relationRepo.batchInsert(id, req.getCategories());
parserHolder.extractTo(getFileType(req.getFileName()), getUploadPath(req.getFileName()),
getExtractPath(getFileNameWithoutExtension(req.getFileName())));
if (CollectionUtils.isNotEmpty(req.getCategories())) {
relationRepo.batchUpdate(id, req.getCategories());
}
if (StringUtils.isNotBlank(req.getFileName())) {
parserHolder.extractTo(getFileType(req.getFileName()), getUploadPath(req.getFileName()),
getExtractPath(getFileNameWithoutExtension(req.getFileName())));
}
return getOperatorById(id);
}
@@ -77,7 +96,7 @@ public class OperatorService {
public OperatorDto uploadOperator(String fileName) {
return parserHolder.parseYamlFromArchive(getFileType(fileName), new File(getUploadPath(fileName)),
OperatorConstant.YAML_PATH);
OperatorConstant.YAML_PATH);
}
public String preUpload() {
@@ -107,4 +126,76 @@ public class OperatorService {
private String getExtractPath(String fileName) {
return operatorBasePath + File.separator + "extract" + File.separator + fileName;
}
private void overrideSettings(OperatorDto operatorDto) {
if (StringUtils.isBlank(operatorDto.getSettings()) || MapUtils.isEmpty(operatorDto.getOverrides())) {
return;
}
try {
Map<String, Map<String, Object>> settings = objectMapper.readValue(operatorDto.getSettings(), Map.class);
for (Map.Entry<String, Object> entry : operatorDto.getOverrides().entrySet()) {
String key = entry.getKey();
if (!settings.containsKey(key)) {
continue;
}
Object value = entry.getValue();
Map<String, Object> setting = settings.get(key);
String type = setting.get("type").toString();
switch (type) {
case "slider":
case "switch":
case "select":
case "input":
case "radio":
setting.put("defaultVal", value);
break;
case "checkbox":
setting.put("defaultVal", convertObjectToListString(value));
break;
case "range":
updateProperties(setting, value);
default:
}
settings.put(key, setting);
}
operatorDto.setSettings(objectMapper.writeValueAsString(settings));
} catch (JsonProcessingException e) {
throw BusinessException.of(OperatorErrorCode.SETTINGS_PARSE_FAILED, e.getMessage());
}
}
private String convertObjectToListString(Object object) {
if (object == null) {
return null;
} else if (object instanceof List<?> list) {
List<String> result = new ArrayList<>();
for (Object item : list) {
result.add(String.valueOf(item));
}
return String.join(",", result);
} else {
return object.toString();
}
}
private void updateProperties(Map<String, Object> setting, Object value) {
List<Object> defaultValue = new ArrayList<>();
if (value instanceof List) {
defaultValue.addAll((List<?>) value);
}
Object properties = setting.get("properties");
if (properties instanceof List<?> list) {
if (defaultValue.size() != list.size()) {
return;
}
List<Map<String, Object>> result = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
Map<String, Object> map = objectMapper.convertValue(list.get(i), Map.class);
map.put("defaultVal", defaultValue.get(i));
result.add(map);
}
setting.put("properties", result);
}
}
}

View File

@@ -28,6 +28,8 @@ public class OperatorConstant {
public static String CATEGORY_ALL_ID = "4d7dbd77-0a92-44f3-9056-2cd62d4a71e4";
public static String CATEGORY_STAR_ID = "51847c24-bba9-11f0-888b-5b143cb738aa";
public static Map<String, String> CATEGORY_MAP = new HashMap<>();
static {

View File

@@ -12,5 +12,7 @@ public interface CategoryRelationRepository extends IRepository<CategoryRelation
void batchInsert(String operatorId, List<String> categories);
void batchUpdate(String operatorId, List<String> categories);
void deleteByOperatorId(String operatorId);
}

View File

@@ -14,4 +14,6 @@ public interface OperatorRepository extends IRepository<Operator> {
void insertOperator(OperatorDto operator);
void deleteOperator(String id);
int countOperatorByStar(boolean isStar);
}

View File

@@ -14,7 +14,9 @@ public enum OperatorErrorCode implements ErrorCode {
YAML_NOT_FOUND("op.0002", "算子中缺少元数据文件"),
FIELD_NOT_FOUND("op.0003", "缺少必要的字段");
FIELD_NOT_FOUND("op.0003", "缺少必要的字段"),
SETTINGS_PARSE_FAILED("op.0004", "settings字段解析失败");
private final String code;
private final String message;

View File

@@ -31,6 +31,17 @@ public class CategoryRelationRepositoryImpl extends CrudRepository<CategoryRelat
mapper.insert(categoryRelations);
}
@Override
public void batchUpdate(String operatorId, List<String> categories) {
List<CategoryRelation> categoryRelations = categories.stream()
.map(category -> new CategoryRelation(category, operatorId))
.toList();
LambdaQueryWrapper<CategoryRelation> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CategoryRelation::getOperatorId, operatorId);
mapper.delete(queryWrapper);
mapper.insert(categoryRelations);
}
@Override
public void deleteByOperatorId(String operatorId) {
LambdaQueryWrapper<CategoryRelation> queryWrapper = new LambdaQueryWrapper<>();

View File

@@ -1,5 +1,6 @@
package com.datamate.operator.infrastructure.persistence.Impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.repository.CrudRepository;
import com.datamate.operator.infrastructure.converter.OperatorConverter;
import com.datamate.operator.domain.model.Operator;
@@ -35,4 +36,11 @@ public class OperatorRepositoryImpl extends CrudRepository<OperatorMapper, Opera
public void deleteOperator(String id) {
mapper.deleteById(id);
}
@Override
public int countOperatorByStar(boolean isStar) {
LambdaQueryWrapper<Operator> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Operator::getIsStar, isStar);
return Math.toIntExact(mapper.selectCount(queryWrapper));
}
}

View File

@@ -27,7 +27,8 @@ public class OperatorViewRepositoryImpl extends CrudRepository<OperatorViewMappe
queryWrapper.in(CollectionUtils.isNotEmpty(categories), "category_id", categories)
.like(StringUtils.isNotBlank(operatorName), "operator_name", operatorName)
.eq(isStar != null, "is_star", isStar)
.groupBy("operator_id");
.groupBy("operator_id")
.orderByDesc("created_at");
Page<OperatorView> queryPage = null;
if (size != null && page != null) {
queryPage = new Page<>(page + 1, size);

View File

@@ -24,7 +24,7 @@ public interface OperatorViewMapper extends BaseMapper<OperatorView> {
@Select("SELECT operator_id AS id, operator_name AS name, description, version, inputs, outputs, runtime, " +
"settings, is_star, created_at, updated_at, " +
"GROUP_CONCAT(category_id ORDER BY created_at DESC SEPARATOR ',') AS categories " +
"GROUP_CONCAT(category_name ORDER BY created_at DESC SEPARATOR ',') AS categories " +
"FROM v_operator WHERE operator_id = #{id}")
OperatorView findOperatorById(@Param("id") String id);
}

View File

@@ -6,6 +6,7 @@ import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* OperatorDto
@@ -32,6 +33,8 @@ public class OperatorDto {
private String settings;
private Map<String, Object> overrides;
private String fileName;
private Boolean isStar;

View File

@@ -2,10 +2,12 @@ package com.datamate.operator.interfaces.rest;
import com.datamate.common.interfaces.PagedResponse;
import com.datamate.operator.application.OperatorService;
import com.datamate.operator.domain.contants.OperatorConstant;
import com.datamate.operator.interfaces.dto.OperatorDto;
import com.datamate.operator.interfaces.dto.OperatorsListPostRequest;
import com.datamate.operator.interfaces.dto.UploadOperatorRequest;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
@@ -19,10 +21,16 @@ public class OperatorController {
@PostMapping("/list")
public PagedResponse<OperatorDto> operatorsListPost(@RequestBody OperatorsListPostRequest request) {
Boolean isStar = null;
List<String> categories = request.getCategories();
if (CollectionUtils.isNotEmpty(request.getCategories()) &&
request.getCategories().contains(OperatorConstant.CATEGORY_STAR_ID)) {
isStar = true;
categories.remove(OperatorConstant.CATEGORY_STAR_ID);
}
List<OperatorDto> responses = operatorService.getOperators(request.getPage(), request.getSize(),
request.getCategories(), request.getOperatorName(), request.getIsStar());
int count = operatorService.getOperatorsCount(request.getCategories(), request.getOperatorName(),
request.getIsStar());
categories, request.getOperatorName(), isStar);
int count = operatorService.getOperatorsCount(categories, request.getOperatorName(), isStar);
int totalPages = (count + request.getSize() + 1) / request.getSize();
return PagedResponse.of(responses, request.getPage(), count, totalPages);
}

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 {

View File

@@ -19,7 +19,7 @@ export default defineConfig({
// },
proxy: {
"^/api": {
target: "http://localhost:8002", // 本地后端服务地址
target: "http://localhost:8080", // 本地后端服务地址
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api/, "/api"),

View File

@@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS t_operator
runtime text,
settings text,
file_name text,
is_star bool,
is_star boolean,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp
);
@@ -63,9 +63,7 @@ VALUES ('64465bec-b46b-11f0-8291-00155d0e4808', '模态', 'modal', 'predefined'
('b5bfc548-8ef6-417c-b8a6-a4197c078249', 'Java', 'java', 'predefined', '873000a2-65b3-474b-8ccc-4813c08c76fb'),
('16e2d99e-eafb-44fc-acd0-f35a2bad28f8', '来源', 'origin', 'predefined', '0'),
('96a3b07a-3439-4557-a835-525faad60ca3', '系统预置', 'predefined', 'predefined', '16e2d99e-eafb-44fc-acd0-f35a2bad28f8'),
('ec2cdd17-8b93-4a81-88c4-ac9e98d10757', '用户上传', 'customized', 'predefined', '16e2d99e-eafb-44fc-acd0-f35a2bad28f8'),
('d8482257-7ee6-41a0-a914-8363c7db1db0', '收藏状态', 'starStatus', 'predefined', '0'),
('79f2d35a-3b6c-4846-a892-2f2015f48f24', '已收藏', 'isStar', 'predefined', 'd8482257-7ee6-41a0-a914-8363c7db1db0');
('ec2cdd17-8b93-4a81-88c4-ac9e98d10757', '用户上传', 'customized', 'predefined', '16e2d99e-eafb-44fc-acd0-f35a2bad28f8');
INSERT IGNORE INTO t_operator
(id, name, description, version, inputs, outputs, runtime, settings, file_name, is_star)
@@ -100,18 +98,18 @@ VALUES ('TextFormatter', 'TXT文本抽取', '抽取TXT中的文本。', '1.0.0',
('AnonymizedUrlCleaner', 'URL网址匿名化', '将文档中的url网址匿名化。', '1.0.0', 'text', 'text', null, null, '', 'false'),
('XMLTagCleaner', 'XML标签去除', '去除XML中的标签。', '1.0.0', 'text', 'text', null, null, '', 'false'),
('ImgFormatter', '读取图片文件', '读取图片文件。', '1.0.0', 'image', 'image', null, null, '', 'false'),
('ImgBlurredImagesCleaner', '模糊图片过滤', '去除模糊的图片。', '1.0.0', 'image', 'image', '{"blurredThreshold": {"name": "梯度函数值", "name_en": "Gradient Value", "description": "梯度函数值取值越小,图片模糊度越高。", "description_en": "A smaller gradient value indicates a higher image blur.", "type": "slider", "defaultVal": 1000, "min": 1, "max": 10000, "step": 1}}', null, '', 'false'),
('ImgBlurredImagesCleaner', '模糊图片过滤', '去除模糊的图片。', '1.0.0', 'image', 'image', null, '{"blurredThreshold": {"name": "梯度函数值", "description": "梯度函数值取值越小,图片模糊度越高。", "type": "slider", "defaultVal": 1000, "min": 1, "max": 10000, "step": 1}}', '', 'false'),
('ImgBrightness', '图片亮度增强', '自适应调节图片的亮度。', '1.0.0', 'image', 'image', null, null, '', 'false'),
('ImgContrast', '图片对比度增强', '自适应调节图片的对比度。', '1.0.0', 'image', 'image', null, null, '', 'false'),
('ImgDenoise', '图片噪点去除', '去除图片中的噪点,主要适用于自然场景。', '1.0.0', 'image', 'image', null, null, '', 'false'),
('ImgDuplicatedImagesCleaner', '重复图片去除', '去除重复的图片。', '1.0.0', 'image', 'image', null, null, '', 'false'),
('ImgPerspectiveTransformation', '图片透视变换', '自适应校正图片的视角,主要适用于文档校正场景。', '1.0.0', 'image', 'image', null, null, '', 'false'),
('ImgResize', '图片重采样', '将图片放大或缩小到指定像素。', '1.0.0', 'image', 'image', '{"targetSize": {"name": "重采样尺寸", "name_en": "Resample Size", "type": "multiple", "properties": [{"type": "inputNumber", "name": "宽度", "name_en": "Width", "description": "像素", "description_en": "Pixel", "defaultVal": 256, "min": 1, "max": 4096, "step": 1}, {"type": "inputNumber", "name": "高度", "name_en": "Height", "description": "像素", "description_en": "Pixel", "defaultVal": 256, "min": 1, "max": 4096, "step": 1}]}}', null, '', 'false'),
('ImgResize', '图片重采样', '将图片放大或缩小到指定像素。', '1.0.0', 'image', 'image', null, '{"targetSize": {"name": "重采样尺寸", "name_en": "Resample Size", "type": "multiple", "properties": [{"type": "inputNumber", "name": "宽度", "description": "像素", "defaultVal": 256, "min": 1, "max": 4096, "step": 1}, {"type": "inputNumber", "name": "高度", "description": "像素", "defaultVal": 256, "min": 1, "max": 4096, "step": 1}]}}', '', 'false'),
('ImgSaturation', '图片饱和度增强', '自适应调节图片的饱和度,主要适用于自然场景图片。', '1.0.0', 'image', 'image', null, null, '', 'false'),
('ImgShadowRemove', '图片阴影去除', '去除图片中的阴影,主要适用于文档场景。', '1.0.0', 'image', 'image', null, null, '', 'false'),
('ImgSharpness', '图片锐度增强', '自适应调节图片的锐度,主要适用于自然场景图片。', '1.0.0', 'image', 'image', null, null, '', 'false'),
('ImgSimilarImagesCleaner', '相似图片去除', '去除相似的图片。', '1.0.0', 'image', 'image', '{"similarThreshold": {"name": "相似度", "name_en": "Similarity", "description": "相似度取值越大,图片相似度越高。", "description_en": "A larger similarity value indicates a higher image similarity.", "type": "slider", "defaultVal": 0.8, "min": 0, "max": 1, "step": 0.01}}', null, '', 'false'),
('ImgTypeUnify', '图片格式转换', '将图片编码格式统一为jpg、jpeg、png、bmp格式。', '1.0.0', 'image', 'image', '{"imgType": {"name": "图片编码格式", "name_en": "Image Encoding Format", "type": "select", "defaultVal": "jpg", "options": [{"label": "jpg", "label_en": "jpg", "value": "jpg"}, {"label": "png", "label_en": "png", "value": "png"}, {"label": "jpeg", "label_en": "jpeg", "value": "jpeg"}, {"label": "bmp", "label_en": "bmp", "value": "bmp"}]}}', null, '', 'false');
('ImgSimilarImagesCleaner', '相似图片去除', '去除相似的图片。', '1.0.0', 'image', 'image', null, '{"similarThreshold": {"name": "相似度", "description": "相似度取值越大,图片相似度越高。", "type": "slider", "defaultVal": 0.8, "min": 0, "max": 1, "step": 0.01}}', '', 'false'),
('ImgTypeUnify', '图片格式转换', '将图片编码格式统一为jpg、jpeg、png、bmp格式。', '1.0.0', 'image', 'image', null, '{"imgType": {"name": "图片编码格式", "type": "select", "defaultVal": "jpg", "options": [{"label": "jpg", "value": "jpg"}, {"label": "png", "value": "png"}, {"label": "jpeg", "value": "jpeg"}, {"label": "bmp", "value": "bmp"}]}}', '', 'false');
INSERT IGNORE INTO t_operator_category_relation(category_id, operator_id)