You've already forked DataMate
fix: 修复入库可能重复;筛选逻辑优化 (#226)
* 修改数据清洗筛选逻辑-筛选修改为多选 * 修改数据清洗筛选逻辑-筛选修改为多选 * antd 组件库样式定制修改 * fix: 修复入库可能重复 * fix: 算子市场筛选逻辑优化 * fix: 清洗任务创建筛选逻辑优化 * fix: 清洗任务创建筛选逻辑优化 --------- Co-authored-by: chase <byzhangxin11@126.com>
This commit is contained in:
@@ -44,7 +44,8 @@ export default function useFetchData<T>(
|
||||
status: [] as string[],
|
||||
tags: [] as string[],
|
||||
// 通用分类筛选(如算子市场的分类 ID 列表)
|
||||
categories: [] as string[],
|
||||
categories: [] as string[][],
|
||||
selectedStar: false,
|
||||
},
|
||||
current: 1,
|
||||
pageSize: 12,
|
||||
@@ -113,11 +114,10 @@ export default function useFetchData<T>(
|
||||
// 同时执行主要数据获取和额外的轮询函数
|
||||
const promises = [
|
||||
fetchFunc({
|
||||
...Object.fromEntries(
|
||||
Object.entries(filter).filter(([_, value]) => value != null && value.length > 0)
|
||||
),
|
||||
categories: filter.categories,
|
||||
...extraParams,
|
||||
keyword,
|
||||
isStar: filter.selectedStar ? true : undefined,
|
||||
type: getFirstOfArray(filter?.type) || undefined,
|
||||
status: getFirstOfArray(filter?.status) || undefined,
|
||||
tags: filter?.tags?.length ? filter.tags.join(",") : undefined,
|
||||
|
||||
@@ -2,21 +2,24 @@ import { StrictMode, Suspense } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { RouterProvider } from "react-router";
|
||||
import router from "./routes/routes";
|
||||
import { App as AntdApp, Spin } from "antd";
|
||||
import { App as AntdApp, Spin, ConfigProvider } from "antd";
|
||||
import "./index.css";
|
||||
import TopLoadingBar from "./components/TopLoadingBar";
|
||||
import { store } from "./store";
|
||||
import { Provider } from "react-redux";
|
||||
import theme from "./theme";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<Provider store={store}>
|
||||
<AntdApp>
|
||||
<Suspense fallback={<Spin />}>
|
||||
<TopLoadingBar />
|
||||
<RouterProvider router={router} />
|
||||
</Suspense>
|
||||
</AntdApp>
|
||||
<ConfigProvider theme={ theme }>
|
||||
<AntdApp>
|
||||
<Suspense fallback={<Spin />}>
|
||||
<TopLoadingBar />
|
||||
<RouterProvider router={router} />
|
||||
</Suspense>
|
||||
</AntdApp>
|
||||
</ConfigProvider>
|
||||
</Provider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@@ -100,57 +100,58 @@ const OperatorLibrary: React.FC<OperatorLibraryProps> = ({
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [showFavorites, setShowFavorites] = useState(false);
|
||||
const [favorites, setFavorites] = useState<Set<string>>(new Set());
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>("all");
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
||||
new Set([])
|
||||
);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string[]>([]);
|
||||
|
||||
// 按分类分组
|
||||
const [operatorListFiltered, setOperatorListFiltered] = useState<OperatorI[]>([]);
|
||||
// 按分类分组
|
||||
const groupedOperators = useMemo(() => {
|
||||
const groups: { [key: string]: OperatorI[] } = {};
|
||||
const groups: { [key: string]: any[] } = {};
|
||||
let operatorFilteredList: OperatorI[];
|
||||
categoryOptions.forEach((cat: any) => {
|
||||
groups[cat.name] = {
|
||||
groups[cat.id] = {
|
||||
...cat,
|
||||
operators: operatorList.filter((op) => op.categories?.includes(cat.id)),
|
||||
};
|
||||
});
|
||||
|
||||
if (selectedCategory && selectedCategory !== "all") {
|
||||
Object.keys(groups).forEach((key) => {
|
||||
if (groups[key].id !== selectedCategory) {
|
||||
delete groups[key];
|
||||
if (selectedCategory.length) {
|
||||
const groupedFiltered: { [key: string]: any[] } = {};
|
||||
selectedCategory.forEach((cat: any) => {
|
||||
let parent = groups[cat].type;
|
||||
if (!groupedFiltered[parent]) {
|
||||
groupedFiltered[parent] = groups[cat].operators
|
||||
} else {
|
||||
groupedFiltered[parent] = Array.from(
|
||||
new Map([...groupedFiltered[parent], ...groups[cat].operators].map(item => [item.id, item])).values()
|
||||
);
|
||||
}
|
||||
})
|
||||
operatorFilteredList = Object.values(groupedFiltered).reduce((acc, currentList) => {
|
||||
if (acc.length === 0) return [];
|
||||
const currentIds = new Set(currentList.map(item => item.id));
|
||||
return acc.filter(item => currentIds.has(item.id));
|
||||
});
|
||||
} else {
|
||||
operatorFilteredList = [...operatorList];
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
Object.keys(groups).forEach((key) => {
|
||||
groups[key].operators = groups[key].operators.filter((operator) =>
|
||||
operator.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
if (groups[key].operators.length === 0) {
|
||||
delete groups[key];
|
||||
}
|
||||
});
|
||||
operatorFilteredList = operatorFilteredList.filter(operator =>
|
||||
operator.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (showFavorites) {
|
||||
Object.keys(groups).forEach((key) => {
|
||||
groups[key].operators = groups[key].operators.filter((operator) =>
|
||||
favorites.has(operator.id)
|
||||
);
|
||||
if (groups[key].operators.length === 0) {
|
||||
delete groups[key];
|
||||
}
|
||||
});
|
||||
operatorFilteredList = operatorFilteredList.filter((operator) =>
|
||||
favorites.has(operator.id)
|
||||
);
|
||||
}
|
||||
|
||||
setExpandedCategories(new Set(Object.keys(groups)));
|
||||
setOperatorListFiltered([...operatorFilteredList]);
|
||||
return groups;
|
||||
}, [categoryOptions, selectedCategory, searchTerm, showFavorites]);
|
||||
|
||||
// 过滤算子
|
||||
const filteredOperators = useMemo(() => {
|
||||
useMemo(() => {
|
||||
return Object.values(groupedOperators).flatMap(
|
||||
(category) => category.operators
|
||||
);
|
||||
@@ -190,17 +191,37 @@ const OperatorLibrary: React.FC<OperatorLibraryProps> = ({
|
||||
setSelectedOperators(newSelected);
|
||||
};
|
||||
|
||||
const handleSelectCategory = (categoryOptions) => {
|
||||
const groups: Record<string, any> = {};
|
||||
const tree: any[] = [];
|
||||
categoryOptions.forEach(item => {
|
||||
const groupName = item.type;
|
||||
if (!groups[groupName]) {
|
||||
const newGroup = {
|
||||
label: groupName,
|
||||
title: groupName,
|
||||
options: []
|
||||
};
|
||||
groups[groupName] = newGroup;
|
||||
tree.push(newGroup);
|
||||
}
|
||||
const { type, ...childItem } = item;
|
||||
groups[groupName].options.push(childItem);
|
||||
});
|
||||
return tree;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-1/4 h-full min-w-3xs flex flex-col">
|
||||
<div className="pb-4 border-b border-gray-200">
|
||||
<span className="flex items-center font-semibold text-base">
|
||||
<Layers className="w-4 h-4 mr-2" />
|
||||
算子库
|
||||
算子库({operatorList.length})
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col h-full pt-4 pr-4 overflow-hidden">
|
||||
{/* 过滤器 */}
|
||||
<div className="flex flex-wrap gap-2 border-b border-gray-100 pb-4">
|
||||
<div className="flex flex-wrap gap-2 border-b border-gray-100 pb-2">
|
||||
<Input
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="搜索算子名称..."
|
||||
@@ -210,8 +231,10 @@ const OperatorLibrary: React.FC<OperatorLibraryProps> = ({
|
||||
/>
|
||||
<Select
|
||||
value={selectedCategory}
|
||||
options={[{ label: "全部分类", value: "all" }, ...categoryOptions]}
|
||||
options={handleSelectCategory(categoryOptions)}
|
||||
onChange={setSelectedCategory}
|
||||
mode="multiple"
|
||||
allowClear
|
||||
className="flex-1"
|
||||
placeholder="选择分类"
|
||||
></Select>
|
||||
@@ -227,53 +250,32 @@ const OperatorLibrary: React.FC<OperatorLibraryProps> = ({
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<div className="flex items-center justify-right w-full">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSelectAll(operatorListFiltered);
|
||||
}}
|
||||
>
|
||||
全选
|
||||
<Tag>{operatorListFiltered.length}</Tag>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* 算子列表 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{/* 分类算子 */}
|
||||
<Collapse
|
||||
ghost
|
||||
activeKey={Array.from(expandedCategories)}
|
||||
onChange={(keys) =>
|
||||
setExpandedCategories(
|
||||
new Set(Array.isArray(keys) ? keys : [keys])
|
||||
)
|
||||
}
|
||||
>
|
||||
{Object.entries(groupedOperators).map(([key, category]) => (
|
||||
<Collapse.Panel
|
||||
key={key}
|
||||
header={
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{category.name}</span>
|
||||
<Tag>{category.operators.length}</Tag>
|
||||
</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSelectAll(category.operators);
|
||||
}}
|
||||
>
|
||||
全选
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<OperatorList
|
||||
selectedOperators={selectedOperators}
|
||||
operators={category.operators}
|
||||
favorites={favorites}
|
||||
toggleOperator={toggleOperator}
|
||||
onDragOperator={handleDragStart}
|
||||
toggleFavorite={toggleFavorite}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
{filteredOperators.length === 0 && (
|
||||
<OperatorList
|
||||
selectedOperators={selectedOperators}
|
||||
operators={operatorListFiltered}
|
||||
favorites={favorites}
|
||||
toggleOperator={toggleOperator}
|
||||
onDragOperator={handleDragStart}
|
||||
toggleFavorite={toggleFavorite}
|
||||
/>
|
||||
|
||||
{operatorListFiltered.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<SearchOutlined className="text-3xl mb-2 opacity-50" />
|
||||
<div>未找到匹配的算子</div>
|
||||
|
||||
@@ -37,10 +37,13 @@ export default function OperatorMarketPage() {
|
||||
|
||||
const [showFilters, setShowFilters] = useState(true);
|
||||
const [categoriesTree, setCategoriesTree] = useState<CategoryTreeI[]>([]);
|
||||
const [starCount, setStarCount] = useState(0);
|
||||
const [selectedStar, setSelectedStar] = useState<boolean>(false);
|
||||
|
||||
const initCategoriesTree = async () => {
|
||||
const { data } = await queryCategoryTreeUsingGet({ page: 0, size: 1000 });
|
||||
setCategoriesTree(data.content || []);
|
||||
setStarCount(data.starCount || 0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -104,16 +107,7 @@ export default function OperatorMarketPage() {
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const filteredIds = Object.values(selectedFilters).reduce(
|
||||
(acc, filter: string[]) => {
|
||||
if (filter.length) {
|
||||
acc.push(...filter);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
const filteredIds = Object.values(selectedFilters).filter(item => item.length > 0);
|
||||
|
||||
// 分类筛选变化时:
|
||||
// 1. 将分类 ID 写入通用 searchParams.filter.categories,确保分页时条件不会丢失
|
||||
@@ -124,9 +118,10 @@ export default function OperatorMarketPage() {
|
||||
filter: {
|
||||
...prev.filter,
|
||||
categories: filteredIds,
|
||||
selectedStar: selectedStar,
|
||||
},
|
||||
}));
|
||||
}, [selectedFilters, setSearchParams]);
|
||||
}, [selectedFilters, setSearchParams, selectedStar]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
@@ -162,8 +157,11 @@ export default function OperatorMarketPage() {
|
||||
<Filters
|
||||
hideFilter={() => setShowFilters(false)}
|
||||
categoriesTree={categoriesTree}
|
||||
selectedStar={selectedStar}
|
||||
starCount={starCount}
|
||||
selectedFilters={selectedFilters}
|
||||
setSelectedFilters={setSelectedFilters}
|
||||
setSelectedStar={setSelectedStar}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-overflow-auto p-6 ">
|
||||
|
||||
@@ -104,15 +104,21 @@ const FilterSection: React.FC<FilterSectionProps> = ({
|
||||
interface FiltersProps {
|
||||
categoriesTree: CategoryTreeI[];
|
||||
selectedFilters: { [key: string]: string[] };
|
||||
selectedStar: boolean;
|
||||
starCount: number;
|
||||
hideFilter: () => void;
|
||||
setSelectedFilters: (filters: { [key: string]: string[] }) => void;
|
||||
setSelectedStar: (item: boolean) => void;
|
||||
}
|
||||
|
||||
const Filters: React.FC<FiltersProps> = ({
|
||||
categoriesTree,
|
||||
selectedFilters,
|
||||
selectedStar,
|
||||
starCount,
|
||||
hideFilter,
|
||||
setSelectedFilters,
|
||||
setSelectedStar,
|
||||
}) => {
|
||||
const clearAllFilters = () => {
|
||||
const newFilters = Object.keys(selectedFilters).reduce((acc, key) => {
|
||||
@@ -126,6 +132,17 @@ const Filters: React.FC<FiltersProps> = ({
|
||||
(filters) => Array.isArray(filters) && filters.length > 0
|
||||
);
|
||||
|
||||
const starCategory = {
|
||||
id: "starStatus",
|
||||
count: starCount,
|
||||
name: "收藏状态",
|
||||
categories: [{
|
||||
id: "isStar",
|
||||
count: starCount,
|
||||
name: "已收藏"
|
||||
}]
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-4 h-full overflow-y-auto">
|
||||
{/* Filter Header */}
|
||||
@@ -170,6 +187,22 @@ const Filters: React.FC<FiltersProps> = ({
|
||||
showIcons={false}
|
||||
/>
|
||||
))}
|
||||
|
||||
<FilterSection
|
||||
key={starCategory.id}
|
||||
total={starCategory.count}
|
||||
title={starCategory.name}
|
||||
options={starCategory.categories.map(cat => ({
|
||||
key: cat.id.toString(),
|
||||
label: cat.name,
|
||||
count: cat.count,
|
||||
}))}
|
||||
selectedValues={selectedStar ? ["isStar"] : []}
|
||||
onSelectionChange={(values) => {
|
||||
values.length > 0 ? setSelectedStar(true) : setSelectedStar(false);
|
||||
}}
|
||||
showIcons={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,11 +46,11 @@ export interface OperatorI {
|
||||
}
|
||||
|
||||
export interface CategoryI {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
count: number; // 该分类下的算子数量
|
||||
type: string; // e.g., "数据源", "数据清洗", "数据分析", "数据可视化"
|
||||
parentId?: number; // 父分类ID,若无父分类则为null
|
||||
parentId?: string; // 父分类ID,若无父分类则为null
|
||||
value: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
7
frontend/src/theme/components/menus.ts
Normal file
7
frontend/src/theme/components/menus.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
const menuTheme = {
|
||||
itemColor: 'rgba(55, 65, 81, 1)',
|
||||
itemSelectedColor: 'rgba(29, 78, 216, 1)',
|
||||
itemSelectedBg: 'rgb(219, 234, 254)',
|
||||
itemBorderRadius: 6,
|
||||
};
|
||||
export default menuTheme;
|
||||
6
frontend/src/theme/components/table.ts
Normal file
6
frontend/src/theme/components/table.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
const tableTheme = {
|
||||
rowSelectedHoverBg: '#F7F9FB',
|
||||
headerColor: 'rgba(100, 116, 139, 1)',
|
||||
headerBg: '#fff',
|
||||
};
|
||||
export default tableTheme;
|
||||
10
frontend/src/theme/index.ts
Normal file
10
frontend/src/theme/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import menuTheme from "./components/menus";
|
||||
import tableTheme from "./components/table";
|
||||
|
||||
const theme = {
|
||||
components: {
|
||||
Menu: menuTheme,
|
||||
Table: tableTheme,
|
||||
},
|
||||
};
|
||||
export default theme;
|
||||
Reference in New Issue
Block a user