You've already forked DataMate
init datamate
This commit is contained in:
181
frontend/src/pages/OperatorMarket/Home/OperatorMarket.tsx
Normal file
181
frontend/src/pages/OperatorMarket/Home/OperatorMarket.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "antd";
|
||||
import { FilterOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { Boxes } from "lucide-react";
|
||||
import { SearchControls } from "@/components/SearchControls";
|
||||
import CardView from "@/components/CardView";
|
||||
import { useNavigate } from "react-router";
|
||||
import type {
|
||||
CategoryTreeI,
|
||||
OperatorI,
|
||||
} from "@/pages/OperatorMarket/operator.model";
|
||||
import Filters from "./components/Filters";
|
||||
import TagManagement from "@/components/TagManagement";
|
||||
import { ListView } from "./components/List";
|
||||
import useFetchData from "@/hooks/useFetchData";
|
||||
import {
|
||||
queryCategoryTreeUsingGet,
|
||||
queryOperatorsUsingPost,
|
||||
} from "../operator.api";
|
||||
import { mapOperator } from "../operator.const";
|
||||
|
||||
export default function OperatorMarketPage() {
|
||||
const navigate = useNavigate();
|
||||
const [viewMode, setViewMode] = useState<"card" | "list">("card");
|
||||
|
||||
const filterOptions = [];
|
||||
|
||||
const [selectedFilters, setSelectedFilters] = useState<
|
||||
Record<string, string[]>
|
||||
>({});
|
||||
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
const [categoriesTree, setCategoriesTree] = useState<CategoryTreeI[]>([]);
|
||||
|
||||
const initCategoriesTree = async () => {
|
||||
const { data } = await queryCategoryTreeUsingGet({ page: 0, size: 1000 });
|
||||
setCategoriesTree(data.content || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initCategoriesTree();
|
||||
}, []);
|
||||
|
||||
const {
|
||||
tableData,
|
||||
pagination,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
fetchData,
|
||||
handleFiltersChange,
|
||||
} = useFetchData(queryOperatorsUsingPost, mapOperator);
|
||||
|
||||
const handleViewOperator = (operator: OperatorI) => {
|
||||
navigate(`/data/operator-market/plugin-detail/${operator.id}`);
|
||||
};
|
||||
|
||||
const handleUploadOperator = () => {
|
||||
navigate(`/data/operator-market/create`);
|
||||
};
|
||||
|
||||
const handleUpdateOperator = (operator: OperatorI) => {
|
||||
navigate(`/data/operator-market/edit/${operator.id}`);
|
||||
};
|
||||
|
||||
const handleDeleteTag = (operator: OperatorI) => {
|
||||
// 删除算子逻辑
|
||||
console.log("删除算子", operator);
|
||||
};
|
||||
|
||||
const operations = [
|
||||
{
|
||||
key: "edit",
|
||||
label: "更新算子",
|
||||
onClick: handleUpdateOperator,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除算子",
|
||||
onClick: handleDeleteTag,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(selectedFilters).length === 0) {
|
||||
return;
|
||||
}
|
||||
const filteredIds = Object.values(selectedFilters).reduce(
|
||||
(acc, filter: string[]) => {
|
||||
if (filter.length) {
|
||||
acc.push(...filter.map(Number));
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
fetchData({ categories: filteredIds?.length ? filteredIds : undefined });
|
||||
}, [selectedFilters]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between">
|
||||
<h1 className="text-xl font-bold text-gray-900">算子市场</h1>
|
||||
{/* <div className="flex gap-2">
|
||||
<TagManagement />
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleUploadOperator}
|
||||
>
|
||||
上传算子
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-1 overflow-auto h-full bg-white rounded-lg">
|
||||
<div
|
||||
className={`border-r border-gray-200 transition-all duration-300 ${
|
||||
showFilters
|
||||
? "translate-x-0 w-56"
|
||||
: "-translate-x-full w-0 opacity-0"
|
||||
}`}
|
||||
>
|
||||
<Filters
|
||||
hideFilter={() => setShowFilters(false)}
|
||||
categoriesTree={categoriesTree}
|
||||
selectedFilters={selectedFilters}
|
||||
setSelectedFilters={setSelectedFilters}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 bg-yellow flex flex-col px-4 my-4">
|
||||
<div className="flex w-full items-top gap-4 border-b border-gray-200 mb-4">
|
||||
{!showFilters && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FilterOutlined />}
|
||||
onClick={() => setShowFilters(true)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<SearchControls
|
||||
className="mb-4"
|
||||
searchTerm={searchParams.keyword}
|
||||
onSearchChange={(keyword) =>
|
||||
setSearchParams({ ...searchParams, keyword })
|
||||
}
|
||||
searchPlaceholder="搜索算子名称、描述..."
|
||||
filters={filterOptions}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
showViewToggle={true}
|
||||
onReload={fetchData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Content */}
|
||||
{tableData.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Boxes className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
没有找到匹配的算子
|
||||
</h3>
|
||||
<p className="text-gray-500">尝试调整筛选条件或搜索关键词</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{viewMode === "card" ? (
|
||||
<CardView data={tableData} pagination={pagination} />
|
||||
) : (
|
||||
<ListView operators={tableData} pagination={pagination} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
179
frontend/src/pages/OperatorMarket/Home/components/Filters.tsx
Normal file
179
frontend/src/pages/OperatorMarket/Home/components/Filters.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import { Button, Checkbox, Tooltip } from "antd";
|
||||
import { FilterOutlined } from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import { CategoryI, CategoryTreeI } from "../../operator.model";
|
||||
|
||||
interface FilterOption {
|
||||
key: string;
|
||||
label: string;
|
||||
count: number;
|
||||
icon?: React.ReactNode;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface FilterSectionProps {
|
||||
title: string;
|
||||
total: number;
|
||||
options: FilterOption[];
|
||||
selectedValues: string[];
|
||||
onSelectionChange: (values: string[]) => void;
|
||||
showIcons?: boolean;
|
||||
badgeColor?: string;
|
||||
}
|
||||
|
||||
const FilterSection: React.FC<FilterSectionProps> = ({
|
||||
total,
|
||||
title,
|
||||
options,
|
||||
selectedValues,
|
||||
onSelectionChange,
|
||||
showIcons = false,
|
||||
}) => {
|
||||
const handleCheckboxChange = (value: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
onSelectionChange([...selectedValues, value]);
|
||||
} else {
|
||||
onSelectionChange(selectedValues.filter((v) => v !== value));
|
||||
}
|
||||
};
|
||||
|
||||
// 全选功能
|
||||
const isAllSelected =
|
||||
options.length > 0 && selectedValues.length === options.length;
|
||||
const isIndeterminate =
|
||||
selectedValues.length > 0 && selectedValues.length < options.length;
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
// 全选
|
||||
onSelectionChange(options.map((option) => option.key));
|
||||
} else {
|
||||
// 全不选
|
||||
onSelectionChange([]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-medium text-gray-900">{title}</h4>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-sm">
|
||||
{/* 全选选项 */}
|
||||
{options.length > 1 && (
|
||||
<label className="flex items-center space-x-2 cursor-pointer border-b border-gray-100 pb-1 ">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
indeterminate={isIndeterminate}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
/>
|
||||
<div className="flex items-center gap-1 flex-1 ml-1">
|
||||
<span className="text-gray-600 font-medium">全选</span>
|
||||
</div>
|
||||
<span className="text-gray-400">({total})</span>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* 各个选项 */}
|
||||
{options.map((option) => (
|
||||
<label
|
||||
key={option.key}
|
||||
className="flex items-center space-x-2 cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedValues.includes(option.key)}
|
||||
onChange={(e) =>
|
||||
handleCheckboxChange(option.key, e.target.checked)
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center gap-1 flex-1 ml-1">
|
||||
{showIcons && option.icon}
|
||||
<span className={`text-gray-700 ${option.color || ""}`}>
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-gray-400">({option.count})</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface FiltersProps {
|
||||
categoriesTree: CategoryTreeI[];
|
||||
selectedFilters: { [key: string]: string[] };
|
||||
hideFilter: () => void;
|
||||
setSelectedFilters: (filters: { [key: string]: string[] }) => void;
|
||||
}
|
||||
|
||||
const Filters: React.FC<FiltersProps> = ({
|
||||
categoriesTree,
|
||||
selectedFilters,
|
||||
hideFilter,
|
||||
setSelectedFilters,
|
||||
}) => {
|
||||
const clearAllFilters = () => {
|
||||
const newFilters = Object.keys(selectedFilters).reduce((acc, key) => {
|
||||
acc[key] = [];
|
||||
return acc;
|
||||
}, {} as { [key: string]: string[] });
|
||||
setSelectedFilters(newFilters);
|
||||
};
|
||||
|
||||
console.log(categoriesTree);
|
||||
|
||||
const hasActiveFilters = Object.values(selectedFilters).some(
|
||||
(filters) => Array.isArray(filters) && filters.length > 0
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4 h-full overflow-y-auto">
|
||||
{/* Filter Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-gray-900 flex items-center gap-2">
|
||||
<Tooltip title="隐藏筛选器">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<FilterOutlined />}
|
||||
onClick={hideFilter}
|
||||
className="cursor-pointer hover:text-blue-500"
|
||||
></Button>
|
||||
</Tooltip>
|
||||
筛选器
|
||||
</h3>
|
||||
{hasActiveFilters && (
|
||||
<span
|
||||
onClick={clearAllFilters}
|
||||
className="cursor-pointer text-sm text-gray-500 hover:text-blue-500"
|
||||
>
|
||||
清除
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Filter Sections */}
|
||||
{categoriesTree.map((category: CategoryTreeI) => (
|
||||
<FilterSection
|
||||
key={category.id}
|
||||
total={category.count}
|
||||
title={category.name}
|
||||
options={category.categories.map((cat: CategoryI) => ({
|
||||
key: cat.id.toString(),
|
||||
label: cat.name,
|
||||
count: cat.count,
|
||||
}))}
|
||||
selectedValues={selectedFilters[category.id] || []}
|
||||
onSelectionChange={(values) =>
|
||||
setSelectedFilters({ ...selectedFilters, [category.id]: values })
|
||||
}
|
||||
showIcons={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Filters;
|
||||
150
frontend/src/pages/OperatorMarket/Home/components/List.tsx
Normal file
150
frontend/src/pages/OperatorMarket/Home/components/List.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Button, Avatar, List, Tag, Badge } from "antd";
|
||||
import { DeleteOutlined, EditOutlined, StarFilled } from "@ant-design/icons";
|
||||
import { Brain, Code, Cpu, Package, Zap, Settings, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Operator } from "../../operator.model";
|
||||
|
||||
export function ListView({ operators, pagination }) {
|
||||
const navigate = useNavigate();
|
||||
const [favoriteOperators, setFavoriteOperators] = useState<Set<number>>(
|
||||
new Set([1, 3, 6])
|
||||
);
|
||||
const handleUpdateOperator = (operator: Operator) => {
|
||||
navigate(`/data/operator-market/create/${operator.id}`);
|
||||
};
|
||||
const handleViewOperator = (operator: Operator) => {
|
||||
navigate(`/data/operator-market/plugin-detail/${operator.id}`);
|
||||
};
|
||||
const handleToggleFavorite = (operatorId: number) => {
|
||||
setFavoriteOperators((prev) => {
|
||||
const newFavorites = new Set(prev);
|
||||
if (newFavorites.has(operatorId)) {
|
||||
newFavorites.delete(operatorId);
|
||||
} else {
|
||||
newFavorites.add(operatorId);
|
||||
}
|
||||
return newFavorites;
|
||||
});
|
||||
};
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusConfig = {
|
||||
active: {
|
||||
label: "活跃",
|
||||
color: "green",
|
||||
icon: <Zap className="w-3 h-3" />,
|
||||
},
|
||||
beta: {
|
||||
label: "测试版",
|
||||
color: "blue",
|
||||
icon: <Settings className="w-3 h-3" />,
|
||||
},
|
||||
deprecated: {
|
||||
label: "已弃用",
|
||||
color: "gray",
|
||||
icon: <X className="w-3 h-3" />,
|
||||
},
|
||||
};
|
||||
return (
|
||||
statusConfig[status as keyof typeof statusConfig] || statusConfig.active
|
||||
);
|
||||
};
|
||||
const getTypeIcon = (type: string) => {
|
||||
const iconMap = {
|
||||
preprocessing: Code,
|
||||
training: Brain,
|
||||
inference: Cpu,
|
||||
postprocessing: Package,
|
||||
};
|
||||
const IconComponent = iconMap[type as keyof typeof iconMap] || Code;
|
||||
return <IconComponent className="w-4 h-4" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<List
|
||||
className="p-4 overflow-auto mx-4"
|
||||
dataSource={operators}
|
||||
pagination={pagination}
|
||||
renderItem={(operator) => (
|
||||
<List.Item
|
||||
className="hover:bg-gray-50 transition-colors px-6 py-4"
|
||||
actions={[
|
||||
<Button
|
||||
key="edit"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => handleUpdateOperator(operator)}
|
||||
icon={<EditOutlined className="w-4 h-4" />}
|
||||
title="更新算子"
|
||||
/>,
|
||||
<Button
|
||||
key="favorite"
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => handleToggleFavorite(operator.id)}
|
||||
className={
|
||||
favoriteOperators.has(operator.id)
|
||||
? "text-yellow-500 hover:text-yellow-600"
|
||||
: "text-gray-400 hover:text-yellow-500"
|
||||
}
|
||||
icon={
|
||||
<StarFilled
|
||||
style={{
|
||||
fontSize: "16px",
|
||||
color: favoriteOperators.has(operator.id)
|
||||
? "#ffcc00ff"
|
||||
: "#d1d5db",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => handleToggleFavorite(operator.id)}
|
||||
/>
|
||||
}
|
||||
title="收藏"
|
||||
/>,
|
||||
<Button
|
||||
key="delete"
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined className="w-4 h-4" />}
|
||||
title="删除算子"
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-blue-100 to-blue-200 rounded-lg flex items-center justify-center">
|
||||
{operator?.icon}
|
||||
</div>
|
||||
}
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className="font-medium text-gray-900 cursor-pointer hover:text-blue-600"
|
||||
onClick={() => handleViewOperator(operator)}
|
||||
>
|
||||
{operator.name}
|
||||
</span>
|
||||
<Tag color="default">v{operator.version}</Tag>
|
||||
<Badge color={getStatusBadge(operator.status).color}>
|
||||
{getStatusBadge(operator.status).label}
|
||||
</Badge>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<div className="space-y-2">
|
||||
<div className="text-gray-600 ">{operator.description}</div>
|
||||
{/* <div className="flex items-center gap-4 text-xs text-gray-500">
|
||||
<span>作者: {operator.author}</span>
|
||||
<span>类型: {operator.type}</span>
|
||||
<span>框架: {operator.framework}</span>
|
||||
<span>使用次数: {operator?.usage?.toLocaleString()}</span>
|
||||
</div> */}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user