diff --git a/frontend/src/hooks/useFetchData.ts b/frontend/src/hooks/useFetchData.ts index 7df77384..c3319c9e 100644 --- a/frontend/src/hooks/useFetchData.ts +++ b/frontend/src/hooks/useFetchData.ts @@ -1,236 +1,238 @@ -// 首页数据获取 -// 支持轮询功能,使用示例: -// const { fetchData, startPolling, stopPolling, isPolling } = useFetchData( -// fetchFunction, -// mapFunction, -// 5000, // 5秒轮询一次,默认30秒 -// true, // 是否自动开始轮询,默认 true -// [fetchStatistics, fetchOtherData] // 额外的轮询函数数组 -// ); -// -// startPolling(); // 开始轮询 -// stopPolling(); // 停止轮询 -// 手动调用 fetchData() 时,如果正在轮询,会重新开始轮询计时 -// 轮询时会同时执行主要的 fetchFunction 和所有额外的轮询函数 -import { useState, useRef, useEffect, useCallback } from "react"; -import { useDebouncedEffect } from "./useDebouncedEffect"; -import Loading from "@/utils/loading"; -import { App } from "antd"; - -export default function useFetchData( - fetchFunc: (params?: any) => Promise, - mapDataFunc: (data: Partial) => T = (data) => data as T, - pollingInterval: number = 30000, // 默认30秒轮询一次 - autoRefresh: boolean = false, // 是否自动开始轮询,默认 false - additionalPollingFuncs: (() => Promise)[] = [], // 额外的轮询函数 - pageOffset: number = 1 -) { - const { message } = App.useApp(); - - // 轮询相关状态 - const [isPolling, setIsPolling] = useState(false); - const pollingTimerRef = useRef(null); - - // 表格数据 - const [tableData, setTableData] = useState([]); - // 设置加载状态 - const [loading, setLoading] = useState(false); - - // 搜索参数 - const [searchParams, setSearchParams] = useState({ - keyword: "", - filter: { - type: [] as string[], - status: [] as string[], - tags: [] as string[], - }, - current: 1, - pageSize: 12, - }); - - // 分页配置 - const [pagination, setPagination] = useState({ - total: 0, - showSizeChanger: true, - pageSizeOptions: ["12", "24", "48"], - showTotal: (total: number) => `共 ${total} 条`, - onChange: (current: number, pageSize?: number) => { - setSearchParams((prev) => ({ - ...prev, - current, - pageSize: pageSize || prev.pageSize, - })); - }, - }); - - const handleFiltersChange = (searchFilters: { [key: string]: string[] }) => { - setSearchParams({ - ...searchParams, - current: 1, - filter: { ...searchParams.filter, ...searchFilters }, - }); - }; - - const handleKeywordChange = (keyword: string) => { - setSearchParams({ - ...searchParams, - current: 1, - keyword: keyword, - }); - }; - - function getFirstOfArray(arr: string[]) { - if (!arr || arr.length === 0 || !Array.isArray(arr)) return undefined; - if (arr[0] === "all") return undefined; - return arr[0]; - } - - // 清除轮询定时器 - const clearPollingTimer = useCallback(() => { - if (pollingTimerRef.current) { - clearTimeout(pollingTimerRef.current); - pollingTimerRef.current = null; - } - }, []); - - const fetchData = useCallback( - async (extraParams = {}, skipPollingRestart = false) => { - const { keyword, filter, current, pageSize } = searchParams; - if (!skipPollingRestart) { - Loading.show(); - setLoading(true); - } - - // 如果正在轮询且不是轮询触发的调用,先停止当前轮询 - const wasPolling = isPolling && !skipPollingRestart; - if (wasPolling) { - clearPollingTimer(); - } - - try { - // 同时执行主要数据获取和额外的轮询函数 - const promises = [ - fetchFunc({ - ...Object.fromEntries( - Object.entries(filter).filter(([_, value]) => value != null && value.length > 0) - ), - ...extraParams, - keyword, - type: getFirstOfArray(filter?.type) || undefined, - status: getFirstOfArray(filter?.status) || undefined, - tags: filter?.tags?.length ? filter.tags.join(",") : undefined, - page: current - pageOffset, - size: pageSize, // Use camelCase for HTTP query params - }), - ...additionalPollingFuncs.map((func) => func()), - ]; - - const results = await Promise.all(promises); - const { data } = results[0]; // 主要数据结果 - - setPagination((prev) => ({ - ...prev, - total: data?.totalElements || 0, - })); - let result = []; - if (mapDataFunc) { - result = data?.content.map(mapDataFunc) ?? []; - } - setTableData(result); - - // 如果之前正在轮询且不是轮询触发的调用,重新开始轮询 - if (wasPolling) { - const poll = () => { - pollingTimerRef.current = setTimeout(() => { - fetchData({}, true).then(() => { - if (pollingTimerRef.current) { - poll(); - } - }); - }, pollingInterval); - }; - poll(); - } - } catch (error) { - console.error(error); - message.error("数据获取失败,请稍后重试"); - } finally { - Loading.hide(); - setLoading(false); - } - }, - [ - searchParams, - fetchFunc, - mapDataFunc, - isPolling, - clearPollingTimer, - pollingInterval, - message, - additionalPollingFuncs, - ] - ); - - // 开始轮询 - const startPolling = useCallback(() => { - clearPollingTimer(); - setIsPolling(true); - - const poll = () => { - pollingTimerRef.current = setTimeout(() => { - fetchData({}, true).then(() => { - if (pollingTimerRef.current) { - poll(); - } - }); - }, pollingInterval); - }; - - poll(); - }, [pollingInterval, clearPollingTimer, fetchData]); - - // 停止轮询 - const stopPolling = useCallback(() => { - clearPollingTimer(); - setIsPolling(false); - }, [clearPollingTimer]); - - // 搜索参数变化时,自动刷新数据 - // keyword 变化时,防抖500ms后刷新 - useDebouncedEffect( - () => { - fetchData(); - }, - [searchParams], - searchParams?.keyword ? 500 : 0 - ); - - // 组件卸载时清理轮询 - useEffect(() => { - if (autoRefresh) { - startPolling(); - } - return () => { - clearPollingTimer(); - }; - }, [clearPollingTimer]); - - return { - loading, - tableData, - pagination: { - ...pagination, - current: searchParams.current, - pageSize: searchParams.pageSize, - }, - searchParams, - setSearchParams, - setPagination, - handleFiltersChange, - handleKeywordChange, - fetchData, - isPolling, - startPolling, - stopPolling, - }; -} +// 首页数据获取 +// 支持轮询功能,使用示例: +// const { fetchData, startPolling, stopPolling, isPolling } = useFetchData( +// fetchFunction, +// mapFunction, +// 5000, // 5秒轮询一次,默认30秒 +// true, // 是否自动开始轮询,默认 true +// [fetchStatistics, fetchOtherData] // 额外的轮询函数数组 +// ); +// +// startPolling(); // 开始轮询 +// stopPolling(); // 停止轮询 +// 手动调用 fetchData() 时,如果正在轮询,会重新开始轮询计时 +// 轮询时会同时执行主要的 fetchFunction 和所有额外的轮询函数 +import { useState, useRef, useEffect, useCallback } from "react"; +import { useDebouncedEffect } from "./useDebouncedEffect"; +import Loading from "@/utils/loading"; +import { App } from "antd"; + +export default function useFetchData( + fetchFunc: (params?: any) => Promise, + mapDataFunc: (data: Partial) => T = (data) => data as T, + pollingInterval: number = 30000, // 默认30秒轮询一次 + autoRefresh: boolean = false, // 是否自动开始轮询,默认 false + additionalPollingFuncs: (() => Promise)[] = [], // 额外的轮询函数 + pageOffset: number = 1 +) { + const { message } = App.useApp(); + + // 轮询相关状态 + const [isPolling, setIsPolling] = useState(false); + const pollingTimerRef = useRef(null); + + // 表格数据 + const [tableData, setTableData] = useState([]); + // 设置加载状态 + const [loading, setLoading] = useState(false); + + // 搜索参数 + const [searchParams, setSearchParams] = useState({ + keyword: "", + filter: { + type: [] as string[], + status: [] as string[], + tags: [] as string[], + // 通用分类筛选(如算子市场的分类 ID 列表) + categories: [] as string[], + }, + current: 1, + pageSize: 12, + }); + + // 分页配置 + const [pagination, setPagination] = useState({ + total: 0, + showSizeChanger: true, + pageSizeOptions: ["12", "24", "48"], + showTotal: (total: number) => `共 ${total} 条`, + onChange: (current: number, pageSize?: number) => { + setSearchParams((prev) => ({ + ...prev, + current, + pageSize: pageSize || prev.pageSize, + })); + }, + }); + + const handleFiltersChange = (searchFilters: { [key: string]: string[] }) => { + setSearchParams({ + ...searchParams, + current: 1, + filter: { ...searchParams.filter, ...searchFilters }, + }); + }; + + const handleKeywordChange = (keyword: string) => { + setSearchParams({ + ...searchParams, + current: 1, + keyword: keyword, + }); + }; + + function getFirstOfArray(arr: string[]) { + if (!arr || arr.length === 0 || !Array.isArray(arr)) return undefined; + if (arr[0] === "all") return undefined; + return arr[0]; + } + + // 清除轮询定时器 + const clearPollingTimer = useCallback(() => { + if (pollingTimerRef.current) { + clearTimeout(pollingTimerRef.current); + pollingTimerRef.current = null; + } + }, []); + + const fetchData = useCallback( + async (extraParams = {}, skipPollingRestart = false) => { + const { keyword, filter, current, pageSize } = searchParams; + if (!skipPollingRestart) { + Loading.show(); + setLoading(true); + } + + // 如果正在轮询且不是轮询触发的调用,先停止当前轮询 + const wasPolling = isPolling && !skipPollingRestart; + if (wasPolling) { + clearPollingTimer(); + } + + try { + // 同时执行主要数据获取和额外的轮询函数 + const promises = [ + fetchFunc({ + ...Object.fromEntries( + Object.entries(filter).filter(([_, value]) => value != null && value.length > 0) + ), + ...extraParams, + keyword, + type: getFirstOfArray(filter?.type) || undefined, + status: getFirstOfArray(filter?.status) || undefined, + tags: filter?.tags?.length ? filter.tags.join(",") : undefined, + page: current - pageOffset, + size: pageSize, // Use camelCase for HTTP query params + }), + ...additionalPollingFuncs.map((func) => func()), + ]; + + const results = await Promise.all(promises); + const { data } = results[0]; // 主要数据结果 + + setPagination((prev) => ({ + ...prev, + total: data?.totalElements || 0, + })); + let result = []; + if (mapDataFunc) { + result = data?.content.map(mapDataFunc) ?? []; + } + setTableData(result); + + // 如果之前正在轮询且不是轮询触发的调用,重新开始轮询 + if (wasPolling) { + const poll = () => { + pollingTimerRef.current = setTimeout(() => { + fetchData({}, true).then(() => { + if (pollingTimerRef.current) { + poll(); + } + }); + }, pollingInterval); + }; + poll(); + } + } catch (error) { + console.error(error); + message.error("数据获取失败,请稍后重试"); + } finally { + Loading.hide(); + setLoading(false); + } + }, + [ + searchParams, + fetchFunc, + mapDataFunc, + isPolling, + clearPollingTimer, + pollingInterval, + message, + additionalPollingFuncs, + ] + ); + + // 开始轮询 + const startPolling = useCallback(() => { + clearPollingTimer(); + setIsPolling(true); + + const poll = () => { + pollingTimerRef.current = setTimeout(() => { + fetchData({}, true).then(() => { + if (pollingTimerRef.current) { + poll(); + } + }); + }, pollingInterval); + }; + + poll(); + }, [pollingInterval, clearPollingTimer, fetchData]); + + // 停止轮询 + const stopPolling = useCallback(() => { + clearPollingTimer(); + setIsPolling(false); + }, [clearPollingTimer]); + + // 搜索参数变化时,自动刷新数据 + // keyword 变化时,防抖500ms后刷新 + useDebouncedEffect( + () => { + fetchData(); + }, + [searchParams], + searchParams?.keyword ? 500 : 0 + ); + + // 组件卸载时清理轮询 + useEffect(() => { + if (autoRefresh) { + startPolling(); + } + return () => { + clearPollingTimer(); + }; + }, [clearPollingTimer]); + + return { + loading, + tableData, + pagination: { + ...pagination, + current: searchParams.current, + pageSize: searchParams.pageSize, + }, + searchParams, + setSearchParams, + setPagination, + handleFiltersChange, + handleKeywordChange, + fetchData, + isPolling, + startPolling, + stopPolling, + }; +} diff --git a/frontend/src/pages/OperatorMarket/Home/OperatorMarket.tsx b/frontend/src/pages/OperatorMarket/Home/OperatorMarket.tsx index 114ce9df..807587ce 100644 --- a/frontend/src/pages/OperatorMarket/Home/OperatorMarket.tsx +++ b/frontend/src/pages/OperatorMarket/Home/OperatorMarket.tsx @@ -1,215 +1,223 @@ -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 - >({}); - - const [showFilters, setShowFilters] = useState(true); - const [categoriesTree, setCategoriesTree] = useState([]); - - 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: , - onClick: handleUpdateOperator, - }, - { - key: "delete", - label: "删除", - danger: true, - icon: , - 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 ( -
- {/* Header */} -
-

算子市场

-
- {/**/} - - -
-
- {/* Main Content */} -
-
- setShowFilters(false)} - categoriesTree={categoriesTree} - selectedFilters={selectedFilters} - setSelectedFilters={setSelectedFilters} - /> -
-
-
- {!showFilters && ( -
- {/* Content */} - {tableData.length === 0 ? ( -
- -

- 没有找到匹配的算子 -

-

尝试调整筛选条件或搜索关键词

-
- ) : ( - <> - {viewMode === "card" ? ( - navigate(`/data/operator-market/plugin-detail/${item.id}`)} - /> - ) : ( - - )} - - )} -
-
-
- ); -} +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 + >({}); + + const [showFilters, setShowFilters] = useState(true); + const [categoriesTree, setCategoriesTree] = useState([]); + + 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: , + onClick: handleUpdateOperator, + }, + { + key: "delete", + label: "删除", + danger: true, + icon: , + 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 ( +
+ {/* Header */} +
+

算子市场

+
+ {/**/} + + +
+
+ {/* Main Content */} +
+
+ setShowFilters(false)} + categoriesTree={categoriesTree} + selectedFilters={selectedFilters} + setSelectedFilters={setSelectedFilters} + /> +
+
+
+ {!showFilters && ( +
+ {/* Content */} + {tableData.length === 0 ? ( +
+ +

+ 没有找到匹配的算子 +

+

尝试调整筛选条件或搜索关键词

+
+ ) : ( + <> + {viewMode === "card" ? ( + navigate(`/data/operator-market/plugin-detail/${item.id}`)} + /> + ) : ( + + )} + + )} +
+
+
+ ); +}