You've already forked DataMate
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:
@@ -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>
|
||||||
|
|||||||
@@ -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>)[] = [], // 额外的轮询函数
|
||||||
|
|||||||
185
frontend/src/hooks/useSliceUpload.tsx
Normal file
185
frontend/src/hooks/useSliceUpload.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
161
frontend/src/mock/mock-seed/knowledge-base.cjs
Normal file
161
frontend/src/mock/mock-seed/knowledge-base.cjs
Normal 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 });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -98,4 +98,5 @@ export interface TaskItem {
|
|||||||
controller: AbortController;
|
controller: AbortController;
|
||||||
cancelFn?: () => void;
|
cancelFn?: () => void;
|
||||||
updateEvent?: string;
|
updateEvent?: string;
|
||||||
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 />}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: "配置文件" },
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user