add operator create page (#38)

* feat: Update site name to DataMate and refine text for AI data processing

* feat: Refactor settings page and implement model access functionality

- Created a new ModelAccess component for managing model configurations.
- Removed the old Settings component and replaced it with a new SettingsPage component that integrates ModelAccess, SystemConfig, and WebhookConfig.
- Added SystemConfig component for managing system settings.
- Implemented WebhookConfig component for managing webhook configurations.
- Updated API functions for model management in settings.apis.ts.
- Adjusted routing to point to the new SettingsPage component.

* feat: Implement Data Collection Page with Task Management and Execution Log

- Created DataCollectionPage component to manage data collection tasks.
- Added TaskManagement and ExecutionLog components for task handling and logging.
- Integrated task operations including start, stop, edit, and delete functionalities.
- Implemented filtering and searching capabilities in task management.
- Introduced SimpleCronScheduler for scheduling tasks with cron expressions.
- Updated CreateTask component to utilize new scheduling and template features.
- Enhanced BasicInformation component to conditionally render fields based on visibility settings.
- Refactored ImportConfiguration component to remove NAS import section.

* feat: Update task creation API endpoint and enhance task creation form with new fields and validation

* Refactor file upload and operator management components

- Removed unnecessary console logs from file download and export functions.
- Added size property to TaskItem interface for better task management.
- Simplified TaskUpload component by utilizing useFileSliceUpload hook for file upload logic.
- Enhanced OperatorPluginCreate component to handle file uploads and parsing more efficiently.
- Updated ConfigureStep component to use Ant Design Form for better data handling and validation.
- Improved PreviewStep component to navigate back to the operator market.
- Added support for additional file types in UploadStep component.
- Implemented delete operator functionality in OperatorMarketPage with confirmation prompts.
- Cleaned up unused API functions in operator.api.ts to streamline the codebase.
- Fixed number formatting utility to handle zero values correctly.
This commit is contained in:
chenghh-9609
2025-10-30 16:30:01 +08:00
committed by GitHub
parent e0884ab048
commit 5612c7cd91
22 changed files with 640 additions and 979 deletions

View File

@@ -246,10 +246,10 @@ function CardView<T extends BaseCardDataType>(props: CardViewProps<T>) {
<div className="grid grid-cols-2 gap-4 py-3"> <div className="grid grid-cols-2 gap-4 py-3">
{item?.statistics?.map((stat, idx) => ( {item?.statistics?.map((stat, idx) => (
<div key={idx}> <div key={idx}>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500 overflow-hidden whitespace-nowrap text-ellipsis w-full">
{stat?.label}: {stat?.label}:
</div> </div>
<div className="text-base font-semibold text-gray-900"> <div className="text-base font-semibold text-gray-900 overflow-hidden whitespace-nowrap text-ellipsis w-full">
{stat?.value} {stat?.value}
</div> </div>
</div> </div>

View File

@@ -16,11 +16,10 @@ import { useState, useRef, useEffect, useCallback } from "react";
import { useDebouncedEffect } from "./useDebouncedEffect"; import { useDebouncedEffect } from "./useDebouncedEffect";
import Loading from "@/utils/loading"; import Loading from "@/utils/loading";
import { App } from "antd"; import { App } from "antd";
import { AnyObject } from "antd/es/_util/type";
export default function useFetchData<T>( export default function useFetchData<T>(
fetchFunc: (params?: any) => Promise<any>, fetchFunc: (params?: any) => Promise<any>,
mapDataFunc: (data: AnyObject) => T = (data) => data as T, mapDataFunc: (data: Partial<T>) => T = (data) => data as T,
pollingInterval: number = 30000, // 默认30秒轮询一次 pollingInterval: number = 30000, // 默认30秒轮询一次
autoRefresh: boolean = true, autoRefresh: boolean = true,
additionalPollingFuncs: (() => Promise<any>)[] = [], // 额外的轮询函数 additionalPollingFuncs: (() => Promise<any>)[] = [], // 额外的轮询函数

View File

@@ -0,0 +1,185 @@
import { TaskItem } from "@/pages/DataManagement/dataset.model";
import { calculateSHA256, checkIsFilesExist } from "@/utils/file.util";
import { App } from "antd";
import { useRef, useState } from "react";
export function useFileSliceUpload(
{
preUpload,
uploadChunk,
cancelUpload,
}: {
preUpload: (id: string, params: any) => Promise<{ data: number }>;
uploadChunk: (id: string, formData: FormData, config: any) => Promise<any>;
cancelUpload: ((reqId: number) => Promise<any>) | null;
},
showTaskCenter = true // 上传时是否显示任务中心
) {
const { message } = App.useApp();
const [taskList, setTaskList] = useState<TaskItem[]>([]);
const taskListRef = useRef<TaskItem[]>([]); // 用于固定任务顺序
const createTask = (detail: any = {}) => {
const { dataset } = detail;
const title = `上传数据集: ${dataset.name} `;
const controller = new AbortController();
const task: TaskItem = {
key: dataset.id,
title,
percent: 0,
reqId: -1,
controller,
size: 0,
updateEvent: detail.updateEvent,
};
taskListRef.current = [task, ...taskListRef.current];
setTaskList(taskListRef.current);
return task;
};
const updateTaskList = (task: TaskItem) => {
taskListRef.current = taskListRef.current.map((item) =>
item.key === task.key ? task : item
);
setTaskList(taskListRef.current);
};
const removeTask = (task: TaskItem) => {
const { key } = task;
taskListRef.current = taskListRef.current.filter(
(item) => item.key !== key
);
setTaskList(taskListRef.current);
if (task.isCancel && task.cancelFn) {
task.cancelFn();
}
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
if (showTaskCenter) {
window.dispatchEvent(
new CustomEvent("show:task-popover", { detail: { show: false } })
);
}
};
async function buildFormData({ file, reqId, i, j }) {
const formData = new FormData();
const { slices, name, size } = file;
const checkSum = await calculateSHA256(slices[j]);
formData.append("file", slices[j]);
formData.append("reqId", reqId.toString());
formData.append("fileNo", (i + 1).toString());
formData.append("chunkNo", (j + 1).toString());
formData.append("fileName", name);
formData.append("fileSize", size.toString());
formData.append("totalChunkNum", slices.length.toString());
formData.append("checkSumHex", checkSum);
return formData;
}
async function uploadSlice(task: TaskItem, fileInfo) {
if (!task) {
return;
}
const { reqId, key } = task;
const { loaded, i, j, files, totalSize } = fileInfo;
const formData = await buildFormData({
file: files[i],
i,
j,
reqId,
});
let newTask = { ...task };
await uploadChunk(key, formData, {
onUploadProgress: (e) => {
const loadedSize = loaded + e.loaded;
const curPercent = Number((loadedSize / totalSize) * 100).toFixed(2);
newTask = {
...newTask,
...taskListRef.current.find((item) => item.key === key),
size: loadedSize,
percent: curPercent >= 100 ? 99.99 : curPercent,
};
updateTaskList(newTask);
},
});
}
async function uploadFile({ task, files, totalSize }) {
const { data: reqId } = await preUpload(task.key, {
totalFileNum: files.length,
totalSize,
datasetId: task.key,
});
const newTask: TaskItem = {
...task,
reqId,
isCancel: false,
cancelFn: () => {
task.controller.abort();
cancelUpload?.(reqId);
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
},
};
updateTaskList(newTask);
if (showTaskCenter) {
window.dispatchEvent(
new CustomEvent("show:task-popover", { detail: { show: true } })
);
}
// // 更新数据状态
if (task.updateEvent) window.dispatchEvent(new Event(task.updateEvent));
let loaded = 0;
for (let i = 0; i < files.length; i++) {
const { slices } = files[i];
for (let j = 0; j < slices.length; j++) {
await uploadSlice(newTask, {
loaded,
i,
j,
files,
totalSize,
});
loaded += slices[j].size;
}
}
removeTask(newTask);
}
const handleUpload = async ({ task, files }) => {
const isErrorFile = await checkIsFilesExist(files);
if (isErrorFile) {
message.error("文件被修改或删除,请重新选择文件上传");
removeTask({
...task,
isCancel: false,
...taskListRef.current.find((item) => item.key === task.key),
});
return;
}
try {
const totalSize = files.reduce((acc, file) => acc + file.size, 0);
await uploadFile({ task, files, totalSize });
} catch (err) {
console.error(err);
message.error("文件上传失败,请稍后重试");
removeTask({
...task,
isCancel: true,
...taskListRef.current.find((item) => item.key === task.key),
});
}
};
return {
taskList,
createTask,
removeTask,
handleUpload,
};
}

View File

@@ -115,20 +115,15 @@ const MockAPI = {
batchEvaluationUsingPost: "/evaluation/batch-evaluate", // 批量评测 batchEvaluationUsingPost: "/evaluation/batch-evaluate", // 批量评测
// 知识生成接口 // 知识生成接口
queryKnowledgeBasesUsingPost: "/knowledge/bases", // 获取知识库列表 queryKnowledgeBasesUsingPost: "/knowledge-base/list", // 获取知识库列表
createKnowledgeBaseUsingPost: "/knowledge/bases/create", // 创建知识库 createKnowledgeBaseUsingPost: "/knowledge-base/create", // 创建知识库
queryKnowledgeBaseByIdUsingGet: "/knowledge/bases/:baseId", // 根据ID获取知识库详情 queryKnowledgeBaseByIdUsingGet: "/knowledge-base/:baseId", // 根据ID获取知识库详情
updateKnowledgeBaseByIdUsingPut: "/knowledge/bases/:baseId", // 更新知识库 updateKnowledgeBaseByIdUsingPut: "/knowledge-base/:baseId", // 更新知识库
deleteKnowledgeBaseByIdUsingDelete: "/knowledge/bases/:baseId", // 删除知识库 deleteKnowledgeBaseByIdUsingDelete: "/knowledge-base/:baseId", // 删除知识库
queryKnowledgeGenerationTasksUsingPost: "/knowledge/tasks", // 获取知识生成任务列表 queryKnowledgeGenerationTasksUsingPost: "/knowledge-base/tasks", // 获取知识生成任务列表
createKnowledgeGenerationTaskUsingPost: "/knowledge/tasks/create", // 创建知识生成任务 addKnowledgeGenerationFilesUsingPost: "/knowledge-base/:baseId/files", // 添加文件到知识库
queryKnowledgeGenerationTaskByIdUsingGet: "/knowledge/tasks/:taskId", // 根据ID获取知识生成任务详情 queryKnowledgeGenerationFilesByIdUsingGet: "/knowledge-base/:baseId/files/:fileId", // 根据ID获取知识生成文件详情
updateKnowledgeGenerationTaskByIdUsingPut: "/knowledge/tasks/:taskId", // 更新知识生成任务 deleteKnowledgeGenerationTaskByIdUsingDelete: "/knowledge-base/:baseId/files", // 删除知识生成文件
deleteKnowledgeGenerationTaskByIdUsingDelete: "/knowledge/tasks/:taskId", // 删除知识生成任务
executeKnowledgeGenerationTaskByIdUsingPost:
"/knowledge/tasks/:taskId/execute", // 执行知识生成任务
stopKnowledgeGenerationTaskByIdUsingPost: "/knowledge/tasks/:taskId/stop", // 停止知识生成任务
queryKnowledgeStatisticsUsingGet: "/knowledge/statistics", // 获取知识生成
// 算子市场 // 算子市场
queryOperatorsUsingPost: "/operators/list", // 获取算子列表 queryOperatorsUsingPost: "/operators/list", // 获取算子列表
@@ -137,6 +132,10 @@ const MockAPI = {
createOperatorUsingPost: "/operators/create", // 创建算子 createOperatorUsingPost: "/operators/create", // 创建算子
updateOperatorByIdUsingPut: "/operators/:operatorId", // 更新算子 updateOperatorByIdUsingPut: "/operators/:operatorId", // 更新算子
uploadOperatorUsingPost: "/operators/upload", // 上传算子 uploadOperatorUsingPost: "/operators/upload", // 上传算子
uploadFileChunkUsingPost: "/operators/upload/chunk", // 上传切片
preUploadOperatorUsingPost: "/operators/upload/pre-upload", // 预上传文件
cancelUploadOperatorUsingPut: "/operators/upload/cancel-upload", // 取消上传
createLabelUsingPost: "/operators/labels", // 创建算子标签 createLabelUsingPost: "/operators/labels", // 创建算子标签
queryLabelsUsingGet: "/labels", // 获取算子标签列表 queryLabelsUsingGet: "/labels", // 获取算子标签列表
deleteLabelsUsingDelete: "/labels", // 删除算子标签 deleteLabelsUsingDelete: "/labels", // 删除算子标签
@@ -151,7 +150,6 @@ const MockAPI = {
createModelUsingPost: "/models/create", // 创建模型 createModelUsingPost: "/models/create", // 创建模型
updateModelUsingPut: "/models/:id", // 更新模型 updateModelUsingPut: "/models/:id", // 更新模型
deleteModelUsingDelete: "/models/:id", // 删除模型 deleteModelUsingDelete: "/models/:id", // 删除模型
}; };
module.exports = addMockPrefix("/api", MockAPI); module.exports = addMockPrefix("/api", MockAPI);

View File

@@ -0,0 +1,161 @@
const Mock = require("mockjs");
const API = require("../mock-apis.cjs");
// 知识库数据
function KnowledgeBaseItem() {
return {
id: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
name: Mock.Random.ctitle(5, 15),
description: Mock.Random.csentence(10, 30),
createdBy: Mock.Random.cname(),
updatedBy: Mock.Random.cname(),
embeddingModel: Mock.Random.pick([
"text-embedding-ada-002",
"text-embedding-3-small",
"text-embedding-3-large",
]),
chatModel: Mock.Random.pick(["gpt-3.5-turbo", "gpt-4", "gpt-4-32k"]),
createdAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
updatedAt: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
};
}
const knowledgeBaseList = new Array(50).fill(null).map(KnowledgeBaseItem);
module.exports = function (router) {
// 获取知识库列表
router.post(API.queryKnowledgeBasesUsingPost, (req, res) => {
const { page = 0, size, keyword } = req.body;
let filteredList = knowledgeBaseList;
if (keyword) {
filteredList = knowledgeBaseList.filter(
(kb) => kb.name.includes(keyword) || kb.description.includes(keyword)
);
}
const start = page * size;
const end = start + size;
const totalElements = filteredList.length;
const paginatedList = filteredList.slice(start, end);
res.send({
code: "0",
msg: "Success",
data: {
totalElements,
page,
size,
content: paginatedList,
},
});
});
// 创建知识库
router.post(API.createKnowledgeBaseUsingPost, (req, res) => {
const item = KnowledgeBaseItem();
knowledgeBaseList.unshift(item);
res.status(201).send(item);
});
// 获取知识库详情
router.get(
new RegExp(API.queryKnowledgeBaseByIdUsingGet.replace(":baseId", "(\\w+)")),
(req, res) => {
const id = req.params.baseId;
const item =
knowledgeBaseList.find((kb) => kb.id === id) || KnowledgeBaseItem();
res.send(item);
}
);
// 更新知识库
router.put(API.updateKnowledgeBaseByIdUsingPut, (req, res) => {
const id = req.params.baseId;
const idx = knowledgeBaseList.findIndex((kb) => kb.id === id);
if (idx >= 0) {
knowledgeBaseList[idx] = { ...knowledgeBaseList[idx], ...req.body };
res.status(201).send(knowledgeBaseList[idx]);
} else {
res.status(404).send({ message: "Not found" });
}
});
// 删除知识库
router.delete(API.deleteKnowledgeBaseByIdUsingDelete, (req, res) => {
const id = req.params.baseId;
const idx = knowledgeBaseList.findIndex((kb) => kb.id === id);
if (idx >= 0) {
knowledgeBaseList.splice(idx, 1);
res.status(201).send({ success: true });
} else {
res.status(404).send({ message: "Not found" });
}
});
// 获取知识生成任务列表
router.post(API.queryKnowledgeGenerationTasksUsingPost, (req, res) => {
const tasks = Mock.mock({
"data|10": [
{
id: "@guid",
name: "@ctitle(5,15)",
status: '@pick(["pending","running","success","failed"])',
createdAt: "@datetime",
updatedAt: "@datetime",
progress: "@integer(0,100)",
},
],
total: 10,
current: 1,
pageSize: 10,
});
res.send(tasks);
});
// 添加文件到知识库
router.post(
new RegExp(
API.addKnowledgeGenerationFilesUsingPost.replace(":baseId", "(\\w+)")
),
(req, res) => {
const file = Mock.mock({
id: "@guid",
name: "@ctitle(5,15)",
size: "@integer(1000,1000000)",
status: "uploaded",
createdAt: "@datetime",
});
res.status(201).send(file);
}
);
// 获取知识生成文件详情
router.get(
new RegExp(
API.queryKnowledgeGenerationFilesByIdUsingGet
.replace(":baseId", "(\\w+)")
.replace(":fileId", "(\\w+)")
),
(req, res) => {
const file = Mock.mock({
id: req.params.fileId,
name: "@ctitle(5,15)",
size: "@integer(1000,1000000)",
status: "uploaded",
createdAt: "@datetime",
});
res.send(file);
}
);
// 删除知识生成文件
router.delete(
new RegExp(
API.deleteKnowledgeGenerationTaskByIdUsingDelete.replace(
":baseId",
"(\\w+)"
)
),
(req, res) => {
res.send({ success: true });
}
);
};

View File

@@ -34,6 +34,32 @@ function labelItem() {
const labelList = new Array(50).fill(null).map(labelItem); const labelList = new Array(50).fill(null).map(labelItem);
module.exports = function (router) { module.exports = function (router) {
router.post(API.preUploadOperatorUsingPost, (req, res) => {
res.status(201).send(Mock.Random.guid());
});
// 上传切片
router.post(API.uploadFileChunkUsingPost, (req, res) => {
// res.status(500).send({ message: "Simulated upload failure" });
res.status(201).send({ data: "success" });
});
// 取消上传
router.put(API.cancelUploadOperatorUsingPut, (req, res) => {
res.status(201).send({ data: "success" });
});
router.post(API.uploadOperatorUsingPost, (req, res) => {
res.status(201).send({
code: "0",
msg: "Upload successful",
data: {
operatorId: Mock.Random.guid().replace(/[^a-zA-Z0-9]/g, ""),
// 其他返回数据
},
});
});
// 获取算子标签列表 // 获取算子标签列表
router.get(API.queryLabelsUsingGet, (req, res) => { router.get(API.queryLabelsUsingGet, (req, res) => {
const { page = 0, size = 20, keyword = "" } = req.query; const { page = 0, size = 20, keyword = "" } = req.query;

View File

@@ -1,196 +0,0 @@
export const mockOperators: Operator[] = [
{
id: 1,
name: "图像预处理算子",
version: "1.2.0",
description:
"支持图像缩放、裁剪、旋转、颜色空间转换等常用预处理操作,优化了内存使用和处理速度",
author: "张三",
category: "图像处理",
modality: ["image"],
type: "preprocessing",
tags: ["图像处理", "预处理", "缩放", "裁剪", "旋转"],
createdAt: "2024-01-15",
lastModified: "2024-01-23",
status: "active",
isFavorited: true,
downloads: 1247,
usage: 856,
framework: "PyTorch",
language: "Python",
size: "2.3MB",
dependencies: ["opencv-python", "pillow", "numpy"],
inputFormat: ["jpg", "png", "bmp", "tiff"],
outputFormat: ["jpg", "png", "tensor"],
performance: {
accuracy: 99.5,
speed: "50ms/image",
memory: "128MB",
},
},
{
id: 2,
name: "文本分词算子",
version: "2.1.3",
description:
"基于深度学习的中文分词算子,支持自定义词典,在医学文本上表现优异",
author: "李四",
category: "自然语言处理",
modality: ["text"],
type: "preprocessing",
tags: ["文本处理", "分词", "中文", "NLP", "医学"],
createdAt: "2024-01-10",
lastModified: "2024-01-20",
status: "active",
isFavorited: false,
downloads: 892,
usage: 634,
framework: "TensorFlow",
language: "Python",
size: "15.6MB",
dependencies: ["tensorflow", "jieba", "transformers"],
inputFormat: ["txt", "json", "csv"],
outputFormat: ["json", "txt"],
performance: {
accuracy: 96.8,
speed: "10ms/sentence",
memory: "256MB",
},
},
{
id: 3,
name: "音频特征提取",
version: "1.0.5",
description: "提取音频的MFCC、梅尔频谱、色度等特征,支持多种音频格式",
author: "王五",
category: "音频处理",
modality: ["audio"],
type: "preprocessing",
tags: ["音频处理", "特征提取", "MFCC", "频谱分析"],
createdAt: "2024-01-08",
lastModified: "2024-01-18",
status: "active",
isFavorited: true,
downloads: 456,
usage: 312,
framework: "PyTorch",
language: "Python",
size: "8.9MB",
dependencies: ["librosa", "scipy", "numpy"],
inputFormat: ["wav", "mp3", "flac", "m4a"],
outputFormat: ["npy", "json", "csv"],
performance: {
speed: "2x实时",
memory: "64MB",
},
},
{
id: 4,
name: "视频帧提取算子",
version: "1.3.2",
description: "高效的视频帧提取算子,支持关键帧检测和均匀采样",
author: "赵六",
category: "视频处理",
modality: ["video"],
type: "preprocessing",
tags: ["视频处理", "帧提取", "关键帧", "采样"],
createdAt: "2024-01-05",
lastModified: "2024-01-22",
status: "active",
isFavorited: false,
downloads: 723,
usage: 445,
framework: "OpenCV",
language: "Python",
size: "12.4MB",
dependencies: ["opencv-python", "ffmpeg-python"],
inputFormat: ["mp4", "avi", "mov", "mkv"],
outputFormat: ["jpg", "png", "npy"],
performance: {
speed: "30fps处理",
memory: "512MB",
},
},
{
id: 5,
name: "多模态融合算子",
version: "2.0.1",
description: "支持文本、图像、音频多模态数据融合的深度学习算子",
author: "孙七",
category: "多模态处理",
modality: ["text", "image", "audio"],
type: "training",
tags: ["多模态", "融合", "深度学习", "注意力机制"],
createdAt: "2024-01-12",
lastModified: "2024-01-21",
status: "beta",
isFavorited: false,
downloads: 234,
usage: 156,
framework: "PyTorch",
language: "Python",
size: "45.2MB",
dependencies: ["torch", "transformers", "torchvision", "torchaudio"],
inputFormat: ["json", "jpg", "wav"],
outputFormat: ["tensor", "json"],
performance: {
accuracy: 94.2,
speed: "100ms/sample",
memory: "2GB",
},
},
{
id: 6,
name: "模型推理加速",
version: "1.1.0",
description: "基于TensorRT的模型推理加速算子,支持多种深度学习框架",
author: "周八",
category: "模型优化",
modality: ["image", "text"],
type: "inference",
tags: ["推理加速", "TensorRT", "优化", "GPU"],
createdAt: "2024-01-03",
lastModified: "2024-01-19",
status: "active",
isFavorited: true,
downloads: 567,
usage: 389,
framework: "TensorRT",
language: "Python",
size: "23.7MB",
dependencies: ["tensorrt", "pycuda", "numpy"],
inputFormat: ["onnx", "pb", "pth"],
outputFormat: ["tensor", "json"],
performance: {
speed: "5x加速",
memory: "减少40%",
},
},
{
id: 7,
name: "数据增强算子",
version: "1.4.1",
description: "丰富的数据增强策略,包括几何变换、颜色变换、噪声添加等",
author: "吴九",
category: "数据增强",
modality: ["image"],
type: "preprocessing",
tags: ["数据增强", "几何变换", "颜色变换", "噪声"],
createdAt: "2024-01-01",
lastModified: "2024-01-17",
status: "active",
isFavorited: false,
downloads: 934,
usage: 678,
framework: "Albumentations",
language: "Python",
size: "6.8MB",
dependencies: ["albumentations", "opencv-python", "numpy"],
inputFormat: ["jpg", "png", "bmp"],
outputFormat: ["jpg", "png", "npy"],
performance: {
speed: "20ms/image",
memory: "32MB",
},
},
];

View File

@@ -146,6 +146,7 @@ export default function CollectionTaskCreate() {
const value = e.target.value; const value = e.target.value;
setNewTask({ setNewTask({
...newTask, ...newTask,
syncMode: value,
scheduleExpression: scheduleExpression:
value === SyncMode.SCHEDULED value === SyncMode.SCHEDULED
? scheduleExpression.cronExpression ? scheduleExpression.cronExpression

View File

@@ -92,17 +92,12 @@ export default function DatasetDetail() {
}; };
useEffect(() => { useEffect(() => {
const refreshDataset = () => {
fetchDataset();
};
const refreshData = () => { const refreshData = () => {
handleRefresh(false); handleRefresh(false);
}; };
window.addEventListener("update:dataset", refreshData); window.addEventListener("update:dataset", refreshData);
window.addEventListener("update:dataset-status", () => refreshDataset());
return () => { return () => {
window.removeEventListener("update:dataset", refreshData); window.removeEventListener("update:dataset", refreshData);
window.removeEventListener("update:dataset-status", refreshDataset);
}; };
}, []); }, []);

View File

@@ -49,7 +49,6 @@ export function useFilesOperation(dataset: Dataset) {
}; };
const handleDownloadFile = async (file: DatasetFile) => { const handleDownloadFile = async (file: DatasetFile) => {
console.log("批量下载文件:", selectedFiles);
// 实际导出逻辑 // 实际导出逻辑
await downloadFileByIdUsingGet(dataset.id, file.id, file.fileName); await downloadFileByIdUsingGet(dataset.id, file.id, file.fileName);
// 假设导出成功 // 假设导出成功
@@ -88,7 +87,6 @@ export function useFilesOperation(dataset: Dataset) {
return; return;
} }
// 执行批量导出逻辑 // 执行批量导出逻辑
console.log("批量导出文件:", selectedFiles);
exportDatasetUsingPost(dataset.id, { fileIds: selectedFiles }) exportDatasetUsingPost(dataset.id, { fileIds: selectedFiles })
.then(() => { .then(() => {
message.success({ message.success({

View File

@@ -98,4 +98,5 @@ export interface TaskItem {
controller: AbortController; controller: AbortController;
cancelFn?: () => void; cancelFn?: () => void;
updateEvent?: string; updateEvent?: string;
size?: number;
} }

View File

@@ -3,167 +3,19 @@ import {
preUploadUsingPost, preUploadUsingPost,
uploadFileChunkUsingPost, uploadFileChunkUsingPost,
} from "@/pages/DataManagement/dataset.api"; } from "@/pages/DataManagement/dataset.api";
import { TaskItem } from "@/pages/DataManagement/dataset.model"; import { Button, Empty, Progress } from "antd";
import { calculateSHA256, checkIsFilesExist } from "@/utils/file.util";
import { App, Button, Empty, Progress } from "antd";
import { DeleteOutlined } from "@ant-design/icons"; import { DeleteOutlined } from "@ant-design/icons";
import { useState, useRef, useEffect } from "react"; import { useEffect } from "react";
import { useFileSliceUpload } from "@/hooks/useSliceUpload";
export default function TaskUpload() { export default function TaskUpload() {
const { message } = App.useApp(); const { createTask, taskList, removeTask, handleUpload } = useFileSliceUpload(
const [taskList, setTaskList] = useState<TaskItem[]>([]); {
const taskListRef = useRef<TaskItem[]>([]); // 用于固定任务顺序 preUpload: preUploadUsingPost,
uploadChunk: uploadFileChunkUsingPost,
const createTask = (detail: any = {}) => { cancelUpload: cancelUploadUsingPut,
const { dataset } = detail;
const title = `上传数据集: ${dataset.name} `;
const controller = new AbortController();
const task: TaskItem = {
key: dataset.id,
title,
percent: 0,
reqId: -1,
controller,
updateEvent: detail.updateEvent || "update:dataset",
};
taskListRef.current = [task, ...taskListRef.current];
setTaskList(taskListRef.current);
return task;
};
const updateTaskList = (task: TaskItem) => {
taskListRef.current = taskListRef.current.map((item) =>
item.key === task.key ? task : item
);
setTaskList(taskListRef.current);
};
const removeTask = (task: TaskItem) => {
const { key } = task;
taskListRef.current = taskListRef.current.filter(
(item) => item.key !== key
);
setTaskList(taskListRef.current);
if (task.isCancel && task.cancelFn) {
task.cancelFn();
} }
window.dispatchEvent(new Event(task.updateEvent || "update:dataset")); );
window.dispatchEvent(
new CustomEvent("show:task-popover", { detail: { show: false } })
);
};
async function buildFormData({ file, reqId, i, j }) {
const formData = new FormData();
const { slices, name, size } = file;
const checkSum = await calculateSHA256(slices[j]);
formData.append("file", slices[j]);
formData.append("reqId", reqId.toString());
formData.append("fileNo", (i + 1).toString());
formData.append("chunkNo", (j + 1).toString());
formData.append("fileName", name);
formData.append("fileSize", size.toString());
formData.append("totalChunkNum", slices.length.toString());
formData.append("checkSumHex", checkSum);
return formData;
}
async function uploadSlice(task: TaskItem, fileInfo) {
if (!task) {
return;
}
const { reqId, key, signal } = task;
const { loaded, i, j, files, totalSize } = fileInfo;
const formData = await buildFormData({
file: files[i],
i,
j,
reqId,
});
let newTask = { ...task };
await uploadFileChunkUsingPost(key, formData, {
onUploadProgress: (e) => {
const loadedSize = loaded + e.loaded;
const curPercent = Math.round(loadedSize / totalSize) * 100;
newTask = {
...newTask,
...taskListRef.current.find((item) => item.key === key),
percent: curPercent >= 100 ? 99.99 : curPercent,
};
updateTaskList(newTask);
},
signal,
});
}
async function uploadFile({ task, files, totalSize }) {
const { data: reqId } = await preUploadUsingPost(task.key, {
totalFileNum: files.length,
totalSize,
datasetId: task.key,
});
const newTask: TaskItem = {
...task,
reqId,
isCancel: false,
cancelFn: () => {
task.controller.abort();
cancelUploadUsingPut(reqId);
window.dispatchEvent(new Event(task.updateEvent || "update:dataset"));
},
};
updateTaskList(newTask);
window.dispatchEvent(
new CustomEvent("show:task-popover", { detail: { show: true } })
);
// 更新数据状态
window.dispatchEvent(new Event("update:dataset-status"));
let loaded = 0;
for (let i = 0; i < files.length; i++) {
const { slices } = files[i];
for (let j = 0; j < slices.length; j++) {
await uploadSlice(newTask, {
loaded,
i,
j,
files,
totalSize,
});
loaded += slices[j].size;
}
}
removeTask(newTask);
}
const handleUpload = async ({ task, files }) => {
const isErrorFile = await checkIsFilesExist(files);
if (isErrorFile) {
message.error("文件被修改或删除,请重新选择文件上传");
removeTask({
...task,
isCancel: false,
...taskListRef.current.find((item) => item.key === task.key),
});
return;
}
try {
const totalSize = files.reduce((acc, file) => acc + file.size, 0);
await uploadFile({ task, files, totalSize });
} catch (err) {
console.error(err);
message.error("文件上传失败,请稍后重试");
removeTask({
...task,
isCancel: true,
...taskListRef.current.find((item) => item.key === task.key),
});
}
};
useEffect(() => { useEffect(() => {
const uploadHandler = (e: any) => { const uploadHandler = (e: any) => {
@@ -195,9 +47,6 @@ export default function TaskUpload() {
removeTask({ removeTask({
...task, ...task,
isCancel: true, isCancel: true,
...taskListRef.current.find(
(item) => item.key === task.key
),
}) })
} }
icon={<DeleteOutlined />} icon={<DeleteOutlined />}

View File

@@ -1,4 +1,4 @@
import { Button, Steps } from "antd"; import { Button, App, Steps } from "antd";
import { import {
ArrowLeft, ArrowLeft,
CheckCircle, CheckCircle,
@@ -6,122 +6,102 @@ import {
TagIcon, TagIcon,
Upload, Upload,
} from "lucide-react"; } from "lucide-react";
import { useNavigate } from "react-router"; import { useNavigate, useParams } from "react-router";
import { useCallback, useState } from "react"; import { useEffect, useState } from "react";
import UploadStep from "./components/UploadStep"; import UploadStep from "./components/UploadStep";
import ParsingStep from "./components/ParsingStep"; import ParsingStep from "./components/ParsingStep";
import ConfigureStep from "./components/ConfigureStep"; import ConfigureStep from "./components/ConfigureStep";
import PreviewStep from "./components/PreviewStep"; import PreviewStep from "./components/PreviewStep";
import { useFileSliceUpload } from "@/hooks/useSliceUpload";
interface ParsedOperatorInfo { import {
name: string; createOperatorUsingPost,
version: string; preUploadOperatorUsingPost,
description: string; queryOperatorByIdUsingGet,
author: string; updateOperatorByIdUsingPut,
category: string; uploadOperatorChunkUsingPost,
modality: string[]; uploadOperatorUsingPost,
type: "preprocessing" | "training" | "inference" | "postprocessing"; } from "../operator.api";
framework: string; import { sliceFile } from "@/utils/file.util";
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() { export default function OperatorPluginCreate() {
const navigate = useNavigate(); const navigate = useNavigate();
const { id } = useParams();
const { message } = App.useApp();
const [uploadStep, setUploadStep] = useState< const [uploadStep, setUploadStep] = useState<
"upload" | "parsing" | "configure" | "preview" "upload" | "parsing" | "configure" | "preview"
>("upload"); >("upload");
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]); const [parsedInfo, setParsedInfo] = useState({});
const [parseProgress, setParseProgress] = useState(0);
const [parsedInfo, setParsedInfo] = useState<ParsedOperatorInfo | null>(null);
const [parseError, setParseError] = useState<string | null>(null); const [parseError, setParseError] = useState<string | null>(null);
const { handleUpload, createTask, taskList } = useFileSliceUpload(
{
preUpload: preUploadOperatorUsingPost,
uploadChunk: uploadOperatorChunkUsingPost,
cancelUpload: null,
},
false
);
// 模拟文件上传 // 模拟文件上传
const handleFileUpload = useCallback((files: FileList) => { const handleFileUpload = async (files: FileList) => {
setIsUploading(true); setIsUploading(true);
setParseError(null); setParseError(null);
setUploadStep("parsing");
// 模拟文件上传过程 try {
setTimeout(() => { const fileName = files[0].name;
const fileArray = Array.from(files).map((file) => ({ await handleUpload({
name: file.name, task: createTask({
size: file.size, dataset: { id: "operator-upload", name: "上传算子" },
type: file.type, }),
})); files: [
setUploadedFiles(fileArray); {
setIsUploading(false); originFile: files[0],
setUploadStep("parsing"); slices: sliceFile(files[0]),
startParsing(); name: fileName,
}, 1000); size: files[0].size,
}, []); },
], // 假设只上传一个文件
// 模拟解析过程
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); setParsedInfo({ ...parsedInfo, fileName, percent: 100 }); // 上传完成,进度100%
}, []); // 解析文件过程
const res = await uploadOperatorUsingPost({ fileName });
const handlePublish = () => { setParsedInfo({ ...parsedInfo, ...res.data });
// 模拟发布过程 } catch (err) {
setUploadStep("preview"); setParseError("文件解析失败," + err.data.message);
setTimeout(() => { } finally {
alert("算子发布成功!"); setIsUploading(false);
// 这里可以重置状态或跳转到其他页面 setUploadStep("configure");
}, 2000); }
}; };
const handlePublish = async () => {
try {
if (id) {
await updateOperatorByIdUsingPut(id, parsedInfo!);
} else {
await createOperatorUsingPost(parsedInfo);
}
setUploadStep("preview");
} catch (err) {
message.error("算子发布失败," + err.data.message);
}
};
const onFetchOperator = async (operatorId: string) => {
// 编辑模式,加载已有算子信息逻辑待实现
const { data } = await queryOperatorByIdUsingGet(operatorId);
setParsedInfo(data);
setUploadStep("configure");
};
useEffect(() => {
if (id) {
// 编辑模式,加载已有算子信息逻辑待实现
onFetchOperator(id);
}
}, [id]);
return ( return (
<div className="flex-overflow-auto bg-gray-50"> <div className="flex-overflow-auto bg-gray-50">
{/* Header */} {/* Header */}
@@ -174,13 +154,13 @@ export default function OperatorPluginCreate() {
)} )}
{uploadStep === "parsing" && ( {uploadStep === "parsing" && (
<ParsingStep <ParsingStep
parseProgress={parseProgress} parseProgress={taskList[0]?.percent || parsedInfo.percent || 0}
uploadedFiles={uploadedFiles} uploadedFiles={taskList}
/> />
)} )}
{uploadStep === "configure" && ( {uploadStep === "configure" && (
<ConfigureStep <ConfigureStep
setUploadStep={setUploadStep} setParsedInfo={setParsedInfo}
parseError={parseError} parseError={parseError}
parsedInfo={parsedInfo} parsedInfo={parsedInfo}
/> />
@@ -192,7 +172,6 @@ export default function OperatorPluginCreate() {
{uploadStep === "configure" && ( {uploadStep === "configure" && (
<div className="flex justify-end gap-3 mt-8"> <div className="flex justify-end gap-3 mt-8">
<Button onClick={() => setUploadStep("upload")}></Button> <Button onClick={() => setUploadStep("upload")}></Button>
<Button onClick={() => setUploadStep("preview")}></Button>
<Button type="primary" onClick={handlePublish}> <Button type="primary" onClick={handlePublish}>
</Button> </Button>

View File

@@ -1,274 +1,75 @@
import { Alert, Input, Button } from "antd"; import { Alert, Input, Form } from "antd";
import { CheckCircle, Plus, TagIcon, X } from "lucide-react"; import TextArea from "antd/es/input/TextArea";
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));
};
export default function ConfigureStep({
parsedInfo,
parseError,
setParsedInfo,
}) {
return ( 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 && ( {parseError && (
<Alert <Alert
message="解析过程中发现问题" message="解析过程中发现问题"
description={parseError} description={parseError}
type="warning" type="error"
showIcon showIcon
className="mb-6"
/> />
)} )}
{parsedInfo && ( {parsedInfo && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <Form
layout="vertical"
initialValues={parsedInfo}
onValuesChange={(_, allValues) => {
setParsedInfo({ ...parsedInfo, ...allValues });
}}
>
{/* 基本信息 */} {/* 基本信息 */}
<div className="space-y-4"> <h3 className="text-lg font-semibold text-gray-900"></h3>
<h3 className="text-lg font-semibold text-gray-900"></h3> <Form.Item label="ID" name="id" rules={[{ required: true }]}>
<div className="space-y-3"> <Input value={parsedInfo.id} readOnly />
<div> </Form.Item>
<label className="block text-sm font-medium text-gray-700 mb-1"> <Form.Item label="名称" name="name" rules={[{ required: true }]}>
<Input value={parsedInfo.name} />
</label> </Form.Item>
<div className="p-2 bg-gray-50 rounded border text-gray-900"> <Form.Item label="版本" name="version" rules={[{ required: true }]}>
{parsedInfo.name} <Input value={parsedInfo.version} />
</div> </Form.Item>
</div> <Form.Item
<div> label="描述"
<label className="block text-sm font-medium text-gray-700 mb-1"> name="description"
rules={[{ required: false }]}
</label> >
<div className="p-2 bg-gray-50 rounded border text-gray-900"> <TextArea value={parsedInfo.description} />
{parsedInfo.version} </Form.Item>
</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>
{/* 技术规格 */} <h3 className="text-lg font-semibold text-gray-900 mt-10 mb-2">
<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> </h3>
<div className="flex flex-wrap gap-2"> <div className="border p-4 rounded-lg flex items-center justify-between gap-4">
{selectedTags.map((tag) => ( <div className="flex-1">
<div <span className="bg-[#2196f3] border-radius px-4 py-1 rounded-tl-lg rounded-br-lg text-white">
key={tag}
className="flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium" </span>
> <pre className="p-4 text-sm overflow-auto">
<TagIcon className="w-3 h-3" /> {parsedInfo.inputs}
<span>{tag}</span> </pre>
<button </div>
onClick={() => handleRemoveTag(tag)} <h1 className="text-3xl">VS</h1>
className="ml-1 hover:text-blue-600" <div className="flex-1">
> <span className="bg-[#4caf50] border-radius px-4 py-1 rounded-tl-lg rounded-br-lg text-white">
<X className="w-3 h-3" />
</button> </span>
</div> <pre className=" p-4 text-sm overflow-auto">
))} {parsedInfo.outputs}
</pre>
</div>
</div> </div>
</div>
<h3 className="text-lg font-semibold text-gray-900 mt-8"></h3>
</Form>
)} )}
</> </>
); );

View File

@@ -1,7 +1,9 @@
import { Button } from "antd"; import { Button } from "antd";
import { CheckCircle, Plus, Eye } from "lucide-react"; import { CheckCircle, Plus } from "lucide-react";
import { useNavigate } from "react-router";
export default function PreviewStep({ setUploadStep }) { export default function PreviewStep({ setUploadStep }) {
const navigate = useNavigate();
return ( return (
<div className="text-center py-2"> <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"> <div className="w-24 h-24 mx-auto mb-6 bg-green-50 rounded-full flex items-center justify-center">
@@ -15,9 +17,8 @@ export default function PreviewStep({ setUploadStep }) {
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
</Button> </Button>
<Button type="primary"> <Button type="primary" onClick={() => navigate("/data/operator-market")}>
<Eye className="w-4 h-4 mr-2" />
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@ export default function UploadStep({ isUploading, onUpload }) {
{ ext: ".py", desc: "Python 脚本文件" }, { ext: ".py", desc: "Python 脚本文件" },
{ ext: ".zip", desc: "压缩包文件" }, { ext: ".zip", desc: "压缩包文件" },
{ ext: ".tar.gz", desc: "压缩包文件" }, { ext: ".tar.gz", desc: "压缩包文件" },
{ ext: ".tar", desc: "压缩包文件" },
{ ext: ".whl", desc: "Python Wheel 包" }, { ext: ".whl", desc: "Python Wheel 包" },
{ ext: ".yaml", desc: "配置文件" }, { ext: ".yaml", desc: "配置文件" },
{ ext: ".yml", desc: "配置文件" }, { ext: ".yml", desc: "配置文件" },

View File

@@ -1,7 +1,12 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button } from "antd"; import { Button, message } from "antd";
import { FilterOutlined, PlusOutlined } from "@ant-design/icons"; import {
import { Boxes } from "lucide-react"; DeleteOutlined,
EditOutlined,
FilterOutlined,
PlusOutlined,
} from "@ant-design/icons";
import { Boxes, Edit } from "lucide-react";
import { SearchControls } from "@/components/SearchControls"; import { SearchControls } from "@/components/SearchControls";
import CardView from "@/components/CardView"; import CardView from "@/components/CardView";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
@@ -14,6 +19,7 @@ import TagManagement from "@/components/TagManagement";
import { ListView } from "./components/List"; import { ListView } from "./components/List";
import useFetchData from "@/hooks/useFetchData"; import useFetchData from "@/hooks/useFetchData";
import { import {
deleteOperatorByIdUsingDelete,
queryCategoryTreeUsingGet, queryCategoryTreeUsingGet,
queryOperatorsUsingPost, queryOperatorsUsingPost,
} from "../operator.api"; } from "../operator.api";
@@ -23,8 +29,6 @@ export default function OperatorMarketPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [viewMode, setViewMode] = useState<"card" | "list">("card"); const [viewMode, setViewMode] = useState<"card" | "list">("card");
const filterOptions = [];
const [selectedFilters, setSelectedFilters] = useState< const [selectedFilters, setSelectedFilters] = useState<
Record<string, string[]> Record<string, string[]>
>({}); >({});
@@ -50,33 +54,44 @@ export default function OperatorMarketPage() {
handleFiltersChange, handleFiltersChange,
} = useFetchData(queryOperatorsUsingPost, mapOperator); } = useFetchData(queryOperatorsUsingPost, mapOperator);
const handleViewOperator = (operator: OperatorI) => {
navigate(`/data/operator-market/plugin-detail/${operator.id}`);
};
const handleUploadOperator = () => { const handleUploadOperator = () => {
navigate(`/data/operator-market/create`); navigate(`/data/operator-market/create`);
}; };
const handleUpdateOperator = (operator: OperatorI) => { const handleUpdateOperator = (operator: OperatorI) => {
navigate(`/data/operator-market/edit/${operator.id}`); navigate(`/data/operator-market/create/${operator.id}`);
}; };
const handleDeleteTag = (operator: OperatorI) => { const handleDeleteOperator = async (operator: OperatorI) => {
// 删除算子逻辑 try {
console.log("删除算子", operator); await deleteOperatorByIdUsingDelete(operator.id);
message.success("算子删除成功");
fetchData();
} catch (error) {
message.error("算子删除失败");
}
}; };
const operations = [ const operations = [
{ {
key: "edit", key: "edit",
label: "更新算子", label: "更新",
icon: <EditOutlined />,
onClick: handleUpdateOperator, onClick: handleUpdateOperator,
}, },
{ {
key: "delete", key: "delete",
label: "删除算子", label: "删除",
onClick: handleDeleteTag, danger: true,
icon: <DeleteOutlined />,
confirm: {
title: "确认删除",
description: "此操作不可撤销,是否继续?",
okText: "删除",
okType: "danger",
cancelText: "取消",
},
onClick: handleDeleteOperator,
}, },
]; ];
@@ -87,14 +102,14 @@ export default function OperatorMarketPage() {
const filteredIds = Object.values(selectedFilters).reduce( const filteredIds = Object.values(selectedFilters).reduce(
(acc, filter: string[]) => { (acc, filter: string[]) => {
if (filter.length) { if (filter.length) {
acc.push(...filter.map(Number)); acc.push(...filter);
} }
return acc; return acc;
}, },
[] []
); );
fetchData({ categories: filteredIds?.length ? filteredIds : undefined }); fetchData({ categories: filteredIds?.length ? filteredIds : undefined });
}, [selectedFilters]); }, [selectedFilters]);
@@ -103,7 +118,7 @@ export default function OperatorMarketPage() {
{/* Header */} {/* Header */}
<div className="flex justify-between"> <div className="flex justify-between">
<h1 className="text-xl font-bold text-gray-900"></h1> <h1 className="text-xl font-bold text-gray-900"></h1>
{/* <div className="flex gap-2"> <div className="flex gap-2">
<TagManagement /> <TagManagement />
<Button <Button
type="primary" type="primary"
@@ -112,7 +127,7 @@ export default function OperatorMarketPage() {
> >
</Button> </Button>
</div> */} </div>
</div> </div>
{/* Main Content */} {/* Main Content */}
<div className="flex-overflow-auto flex-row border-card"> <div className="flex-overflow-auto flex-row border-card">
@@ -146,7 +161,7 @@ export default function OperatorMarketPage() {
setSearchParams({ ...searchParams, keyword }) setSearchParams({ ...searchParams, keyword })
} }
searchPlaceholder="搜索算子名称、描述..." searchPlaceholder="搜索算子名称、描述..."
filters={filterOptions} filters={[]}
onFiltersChange={handleFiltersChange} onFiltersChange={handleFiltersChange}
viewMode={viewMode} viewMode={viewMode}
onViewModeChange={setViewMode} onViewModeChange={setViewMode}
@@ -167,9 +182,17 @@ export default function OperatorMarketPage() {
) : ( ) : (
<> <>
{viewMode === "card" ? ( {viewMode === "card" ? (
<CardView data={tableData} pagination={pagination} /> <CardView
data={tableData}
pagination={pagination}
operations={operations}
/>
) : ( ) : (
<ListView operators={tableData} pagination={pagination} /> <ListView
operators={tableData}
operations={operations}
pagination={pagination}
/>
)} )}
</> </>
)} )}

View File

@@ -122,8 +122,6 @@ const Filters: React.FC<FiltersProps> = ({
setSelectedFilters(newFilters); setSelectedFilters(newFilters);
}; };
console.log(categoriesTree);
const hasActiveFilters = Object.values(selectedFilters).some( const hasActiveFilters = Object.values(selectedFilters).some(
(filters) => Array.isArray(filters) && filters.length > 0 (filters) => Array.isArray(filters) && filters.length > 0
); );

View File

@@ -1,11 +1,11 @@
import { Button, List, Tag, Badge } from "antd"; import { Button, List, Tag, Badge } from "antd";
import { DeleteOutlined, EditOutlined, StarFilled } from "@ant-design/icons"; import { StarFilled } from "@ant-design/icons";
import { Zap, Settings, X } from "lucide-react"; import { Zap, Settings, X } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { Operator } from "../../operator.model"; import { Operator } from "../../operator.model";
export function ListView({ operators, pagination }) { export function ListView({ operators = [], pagination, operations }) {
const navigate = useNavigate(); const navigate = useNavigate();
const [favoriteOperators, setFavoriteOperators] = useState<Set<number>>( const [favoriteOperators, setFavoriteOperators] = useState<Set<number>>(
new Set([1, 3, 6]) new Set([1, 3, 6])
@@ -59,46 +59,39 @@ export function ListView({ operators, pagination }) {
<List.Item <List.Item
className="hover:bg-gray-50 transition-colors px-6 py-4" className="hover:bg-gray-50 transition-colors px-6 py-4"
actions={[ actions={[
<Button // <Button
key="edit" // key="favorite"
type="text" // type="text"
size="small" // size="small"
onClick={() => handleUpdateOperator(operator)} // onClick={() => handleToggleFavorite(operator.id)}
icon={<EditOutlined className="w-4 h-4" />} // className={
title="更新算子" // favoriteOperators.has(operator.id)
/>, // ? "text-yellow-500 hover:text-yellow-600"
<Button // : "text-gray-400 hover:text-yellow-500"
key="favorite" // }
type="text" // icon={
size="small" // <StarFilled
onClick={() => handleToggleFavorite(operator.id)} // style={{
className={ // fontSize: "16px",
favoriteOperators.has(operator.id) // color: favoriteOperators.has(operator.id)
? "text-yellow-500 hover:text-yellow-600" // ? "#ffcc00ff"
: "text-gray-400 hover:text-yellow-500" // : "#d1d5db",
} // cursor: "pointer",
icon={ // }}
<StarFilled // onClick={() => handleToggleFavorite(operator.id)}
style={{ // />
fontSize: "16px", // }
color: favoriteOperators.has(operator.id) // title="收藏"
? "#ffcc00ff" // />,
: "#d1d5db", ...operations.map((operation) => (
cursor: "pointer", <Button
}} type="text"
onClick={() => handleToggleFavorite(operator.id)} size="small"
/> title={operation.label}
} icon={operation.icon}
title="收藏" onClick={() => operation.onClick(operator)}
/>, />
<Button )),
key="delete"
type="text"
size="small"
danger
icon={<DeleteOutlined className="w-4 h-4" />}
title="删除算子"
/>,
]} ]}
> >
<List.Item.Meta <List.Item.Meta
@@ -124,12 +117,12 @@ export function ListView({ operators, pagination }) {
description={ description={
<div className="space-y-2"> <div className="space-y-2">
<div className="text-gray-600 ">{operator.description}</div> <div className="text-gray-600 ">{operator.description}</div>
{/* <div className="flex items-center gap-4 text-xs text-gray-500"> <div className="flex items-center gap-4 text-xs text-gray-500">
<span>: {operator.author}</span> <span>: {operator.author}</span>
<span>: {operator.type}</span> <span>: {operator.type}</span>
<span>: {operator.framework}</span> <span>: {operator.framework}</span>
<span>使: {operator?.usage?.toLocaleString()}</span> <span>使: {operator?.usage?.toLocaleString()}</span>
</div> */} </div>
</div> </div>
} }
/> />

View File

@@ -21,7 +21,10 @@ export function createOperatorUsingPost(data: any) {
} }
// 更新算子 // 更新算子
export function updateOperatorByIdUsingPut(operatorId: string | number, data: any) { export function updateOperatorByIdUsingPut(
operatorId: string | number,
data: any
) {
return put(`/api/operators/${operatorId}`, data); return put(`/api/operators/${operatorId}`, data);
} }
@@ -35,6 +38,16 @@ export function uploadOperatorUsingPost(data: any) {
return post("/api/operators/upload", data); return post("/api/operators/upload", data);
} }
export function preUploadOperatorUsingPost(_, data: any) {
return post("/api/operators/upload/pre-upload", data);
}
export function uploadOperatorChunkUsingPost(_, data: FormData, config?: any) {
return post("/api/operators/upload/chunk", data, {
showLoading: false,
...config,
});
}
// 发布算子 // 发布算子
export function publishOperatorUsingPost(operatorId: string | number) { export function publishOperatorUsingPost(operatorId: string | number) {
return post(`/api/operators/${operatorId}/publish`); return post(`/api/operators/${operatorId}/publish`);
@@ -75,169 +88,3 @@ export function deleteCategoryUsingDelete(data: { id: string | number }) {
return del("/api/category", data); 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);
}

View File

@@ -79,6 +79,7 @@ export const formatDuration = (seconds: number): string => {
}; };
export const formatNumber = (num: number): string => { export const formatNumber = (num: number): string => {
if (!num && num !== 0) return "0";
if (num >= 1e9) { if (num >= 1e9) {
return (num / 1e9).toFixed(2).replace(/\.?0+$/, "") + "B"; return (num / 1e9).toFixed(2).replace(/\.?0+$/, "") + "B";
} else if (num >= 1e6) { } else if (num >= 1e6) {