Revert "feat: fix the problem in the Operator Market frontend pages"

This commit is contained in:
Kecheng Sha
2025-12-29 12:00:37 +08:00
committed by GitHub
parent 8f30f71a68
commit 0df7a872e4
213 changed files with 45537 additions and 45547 deletions

View File

@@ -1,223 +1,215 @@
import { useEffect, useState } from "react";
import { Button, message } from "antd";
import {
DeleteOutlined,
EditOutlined,
FilterOutlined,
PlusOutlined,
DownloadOutlined
} 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/business/TagManagement";
import { ListView } from "./components/List";
import useFetchData from "@/hooks/useFetchData";
import {
deleteOperatorByIdUsingDelete,
downloadExampleOperatorUsingGet,
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 [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,
handleKeywordChange,
} = useFetchData(queryOperatorsUsingPost, mapOperator);
const handleUploadOperator = () => {
navigate(`/data/operator-market/create`);
};
const handleDownload = async () => {
await downloadExampleOperatorUsingGet("test_operator.tar");
message.success("文件下载成功");
};
const handleUpdateOperator = (operator: OperatorI) => {
navigate(`/data/operator-market/create/${operator.id}`);
};
const handleDeleteOperator = async (operator: OperatorI) => {
try {
await deleteOperatorByIdUsingDelete(operator.id);
message.success("算子删除成功");
fetchData();
} catch (error) {
message.error("算子删除失败");
}
};
const operations = [
{
key: "edit",
label: "更新",
icon: <EditOutlined />,
onClick: handleUpdateOperator,
},
{
key: "delete",
label: "删除",
danger: true,
icon: <DeleteOutlined />,
confirm: {
title: "确认删除",
description: "此操作不可撤销,是否继续?",
okText: "删除",
okType: "danger",
cancelText: "取消",
},
onClick: handleDeleteOperator,
},
];
useEffect(() => {
const filteredIds = Object.values(selectedFilters).reduce(
(acc, filter: string[]) => {
if (filter.length) {
acc.push(...filter);
}
return acc;
},
[]
);
// 分类筛选变化时:
// 1. 将分类 ID 写入通用 searchParams.filter.categories,确保分页时条件不会丢失
// 2. 将页码重置为 1,避免从“全选”页的当前页跳入细分列表的同一页
setSearchParams((prev) => ({
...prev,
current: 1,
filter: {
...prev.filter,
categories: filteredIds,
},
}));
}, [selectedFilters, setSearchParams]);
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
icon={<DownloadOutlined />}
onClick={handleDownload}
>
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleUploadOperator}
>
</Button>
</div>
</div>
{/* Main Content */}
<div className="flex-overflow-auto flex-row border-card">
<div
className={`border-r border-gray-100 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-overflow-auto p-6 ">
<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 mb-4">
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange}
searchPlaceholder="搜索算子名称、描述..."
filters={[]}
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}
operations={operations}
onView={(item) => navigate(`/data/operator-market/plugin-detail/${item.id}`)}
/>
) : (
<ListView
operators={tableData}
operations={operations}
pagination={pagination}
/>
)}
</>
)}
</div>
</div>
</div>
);
}
import { useEffect, useState } from "react";
import { Button, message } from "antd";
import {
DeleteOutlined,
EditOutlined,
FilterOutlined,
PlusOutlined,
DownloadOutlined
} 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/business/TagManagement";
import { ListView } from "./components/List";
import useFetchData from "@/hooks/useFetchData";
import {
deleteOperatorByIdUsingDelete,
downloadExampleOperatorUsingGet,
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 [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,
fetchData,
handleFiltersChange,
handleKeywordChange,
} = useFetchData(queryOperatorsUsingPost, mapOperator);
const handleUploadOperator = () => {
navigate(`/data/operator-market/create`);
};
const handleDownload = async () => {
await downloadExampleOperatorUsingGet("test_operator.tar");
message.success("文件下载成功");
};
const handleUpdateOperator = (operator: OperatorI) => {
navigate(`/data/operator-market/create/${operator.id}`);
};
const handleDeleteOperator = async (operator: OperatorI) => {
try {
await deleteOperatorByIdUsingDelete(operator.id);
message.success("算子删除成功");
fetchData();
} catch (error) {
message.error("算子删除失败");
}
};
const operations = [
{
key: "edit",
label: "更新",
icon: <EditOutlined />,
onClick: handleUpdateOperator,
},
{
key: "delete",
label: "删除",
danger: true,
icon: <DeleteOutlined />,
confirm: {
title: "确认删除",
description: "此操作不可撤销,是否继续?",
okText: "删除",
okType: "danger",
cancelText: "取消",
},
onClick: handleDeleteOperator,
},
];
useEffect(() => {
if (Object.keys(selectedFilters).length === 0) {
return;
}
const filteredIds = Object.values(selectedFilters).reduce(
(acc, filter: string[]) => {
if (filter.length) {
acc.push(...filter);
}
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
icon={<DownloadOutlined />}
onClick={handleDownload}
>
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleUploadOperator}
>
</Button>
</div>
</div>
{/* Main Content */}
<div className="flex-overflow-auto flex-row border-card">
<div
className={`border-r border-gray-100 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-overflow-auto p-6 ">
<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 mb-4">
<SearchControls
searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange}
searchPlaceholder="搜索算子名称、描述..."
filters={[]}
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}
operations={operations}
onView={(item) => navigate(`/data/operator-market/plugin-detail/${item.id}`)}
/>
) : (
<ListView
operators={tableData}
operations={operations}
pagination={pagination}
/>
)}
</>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,177 +1,177 @@
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);
};
const hasActiveFilters = Object.values(selectedFilters).some(
(filters) => Array.isArray(filters) && filters.length > 0
);
return (
<div className="p-6 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>
<span></span>
</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;
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);
};
const hasActiveFilters = Object.values(selectedFilters).some(
(filters) => Array.isArray(filters) && filters.length > 0
);
return (
<div className="p-6 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>
<span></span>
</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;

View File

@@ -1,90 +1,90 @@
import { Button, List, Tag } from "antd";
import { useNavigate } from "react-router";
import { Operator } from "../../operator.model";
export function ListView({ operators = [], pagination, operations }) {
const navigate = useNavigate();
const handleViewOperator = (operator: Operator) => {
navigate(`/data/operator-market/plugin-detail/${operator.id}`);
};
return (
<List
className="p-4 flex-1 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="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="收藏"
// />,
...operations.map((operation) => (
<Button
type="text"
size="small"
title={operation.label}
icon={operation.icon}
danger={operation.danger}
onClick={() => operation.onClick(operator)}
/>
)),
]}
>
<List.Item.Meta
avatar={
<div className="w-12 h-12 bg-gradient-to-br from-sky-300 to-blue-500 rounded-lg flex items-center justify-center">
<div className="w-8 h-8 text-white">{operator?.icon}</div>
</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>
</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>
)}
/>
);
}
import { Button, List, Tag } from "antd";
import { useNavigate } from "react-router";
import { Operator } from "../../operator.model";
export function ListView({ operators = [], pagination, operations }) {
const navigate = useNavigate();
const handleViewOperator = (operator: Operator) => {
navigate(`/data/operator-market/plugin-detail/${operator.id}`);
};
return (
<List
className="p-4 flex-1 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="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="收藏"
// />,
...operations.map((operation) => (
<Button
type="text"
size="small"
title={operation.label}
icon={operation.icon}
danger={operation.danger}
onClick={() => operation.onClick(operator)}
/>
)),
]}
>
<List.Item.Meta
avatar={
<div className="w-12 h-12 bg-gradient-to-br from-sky-300 to-blue-500 rounded-lg flex items-center justify-center">
<div className="w-8 h-8 text-white">{operator?.icon}</div>
</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>
</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>
)}
/>
);
}