From 6a2763c38ed627344bf217cd620177f60c09bf72 Mon Sep 17 00:00:00 2001 From: Dallas98 <990259227@qq.com> Date: Mon, 2 Mar 2026 21:35:19 +0800 Subject: [PATCH] fix: improve filter synchronization and data fetching logic in SearchControls and useFetchData --- frontend/src/components/SearchControls.tsx | 16 +- frontend/src/hooks/useFetchData.ts | 555 +++++++++++---------- frontend/vite.config.ts | 4 +- 3 files changed, 306 insertions(+), 269 deletions(-) diff --git a/frontend/src/components/SearchControls.tsx b/frontend/src/components/SearchControls.tsx index 7ae00c6d..3790137b 100644 --- a/frontend/src/components/SearchControls.tsx +++ b/frontend/src/components/SearchControls.tsx @@ -96,6 +96,8 @@ export function SearchControls({ } else { // 非受控模式:更新内部状态 setInternalSelectedFilters(newFilters); + // 同时通知父组件当前的筛选值,避免依赖 useEffect 造成死循环 + onFiltersChange?.(newFilters); } }; @@ -140,20 +142,6 @@ export function SearchControls({ (values) => Array.isArray(values) && values.length > 0 && values[0] !== undefined ); - // 同步外部 selectedFilters 到内部状态 - useEffect(() => { - if (externalSelectedFilters !== undefined) { - setInternalSelectedFilters(externalSelectedFilters); - } - }, [externalSelectedFilters]); - - // 非受控模式下,当内部状态变化时通知父组件 - useEffect(() => { - if (externalSelectedFilters !== undefined) return; // 受控模式不需要这个 effect - if (Object.keys(selectedFilters).length === 0) return; - onFiltersChange?.(selectedFilters); - }, [selectedFilters, onFiltersChange, externalSelectedFilters]); - return (
diff --git a/frontend/src/hooks/useFetchData.ts b/frontend/src/hooks/useFetchData.ts index aa37afef..79084e4b 100644 --- a/frontend/src/hooks/useFetchData.ts +++ b/frontend/src/hooks/useFetchData.ts @@ -1,253 +1,302 @@ -// 首页数据获取 -// 支持轮询功能,使用示例: -// 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"; -import { useTranslation } from "react-i18next"; - -export default function useFetchData( - fetchFunc: (params?: any) => Promise, - mapDataFunc: (data: Partial) => T = (data) => data as T, - pollingInterval: number = 30000, // Default polling interval 30 seconds - autoRefresh: boolean = false, // Whether to auto start polling, default false - additionalPollingFuncs: (() => Promise)[] = [], // Additional polling functions - pageOffset: number = 1 -) { - const { message } = App.useApp(); - const { t } = useTranslation(); - - // 轮询相关状态 - 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[][], - selectedStar: false, - }, - current: 1, - pageSize: 12, - }); - - // Pagination configuration - const [pagination, setPagination] = useState({ - total: 0, - showSizeChanger: true, - pageSizeOptions: ["12", "24", "48"], - showTotal: (total: number) => `${t('hooks.fetchData.totalItems')}: ${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 apiParams = { - categories: filter.categories, - ...extraParams, - keyword, - isStar: filter.selectedStar ? true : undefined, - type: getFirstOfArray(filter?.type) || undefined, - status: getFirstOfArray(filter?.status) || undefined, - built_in: filter?.builtIn !== undefined ? (getFirstOfArray(filter?.builtIn) === "true") : undefined, - tags: filter?.tags?.length ? filter.tags.join(",") : undefined, - page: current - pageOffset, - size: pageSize, // Use camelCase for HTTP query params - }; - - // 添加可能存在的额外参数(如 start_time, end_time 等) - Object.keys(searchParams).forEach(key => { - if (!['keyword', 'filter', 'current', 'pageSize'].includes(key) && apiParams[key as keyof typeof apiParams] === undefined) { - (apiParams as any)[key] = (searchParams as any)[key]; - } - }); - - const promises = [ - fetchFunc(apiParams), - ...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) { - if (error.status === 401) { - message.warn(t('hooks.fetchData.loginRequired')); - } else { - message.error(t('hooks.fetchData.fetchFailed')); - } - } 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, useMemo } from "react"; +import { useDebouncedEffect } from "./useDebouncedEffect"; +import Loading from "@/utils/loading"; +import { App } from "antd"; +import { useTranslation } from "react-i18next"; + +export default function useFetchData( + fetchFunc: (params?: any) => Promise, + mapDataFunc: (data: Partial) => T = (data) => data as T, + pollingInterval: number = 30000, // Default polling interval 30 seconds + autoRefresh: boolean = false, // Whether to auto start polling, default false + additionalPollingFuncs: (() => Promise)[] = [], // Additional polling functions + pageOffset: number = 1 +) { + const { message } = App.useApp(); + const { t } = useTranslation(); + + // 轮询相关状态 + const [isPolling, setIsPolling] = useState(false); + const pollingTimerRef = useRef(null); + const isPollingRef = useRef(false); + + // 表格数据 + const [tableData, setTableData] = useState([]); + // 设置加载状态 + const [loading, setLoading] = useState(false); + + // 搜索参数 + const [searchParams, setSearchParams] = useState({ + keyword: "", + filter: { + type: [] as string[], + status: [] as string[], + tags: [] as string[], + categories: [] as string[][], + selectedStar: false, + }, + current: 1, + pageSize: 12, + }); + + // 使用 ref 存储 searchParams 的值 + const searchParamsRef = useRef(searchParams); + searchParamsRef.current = searchParams; + + // 组件挂载状态 ref + const isMountedRef = useRef(true); + + // 跟踪上一次的 searchParams,用于避免重复请求 + const prevSearchParamsRef = useRef(""); + + // Pagination configuration + const [pagination, setPagination] = useState({ + total: 0, + showSizeChanger: true, + pageSizeOptions: ["12", "24", "48"], + showTotal: (total: number) => `${t('hooks.fetchData.totalItems')}: ${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 } = searchParamsRef.current; + + if (!skipPollingRestart) { + Loading.show(); + setLoading(true); + } + + const wasPolling = isPollingRef.current && !skipPollingRestart; + if (wasPolling) { + clearPollingTimer(); + } + + try { + const apiParams = { + categories: filter.categories, + ...extraParams, + keyword, + isStar: filter.selectedStar ? true : undefined, + type: getFirstOfArray(filter?.type) || undefined, + status: getFirstOfArray(filter?.status) || undefined, + built_in: filter?.builtIn !== undefined ? (getFirstOfArray(filter?.builtIn) === "true") : undefined, + tags: filter?.tags?.length ? filter.tags.join(",") : undefined, + page: current - pageOffset, + size: pageSize, + }; + + Object.keys(searchParamsRef.current).forEach(key => { + if (!['keyword', 'filter', 'current', 'pageSize'].includes(key) && apiParams[key as keyof typeof apiParams] === undefined) { + (apiParams as any)[key] = (searchParamsRef.current as any)[key]; + } + }); + + const promises = [ + fetchFunc(apiParams), + ...additionalPollingFuncs.map((func) => func()), + ]; + + const results = await Promise.all(promises); + const { data } = results[0]; + + if (!isMountedRef.current) { + return; + } + + 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 && isMountedRef.current) { + poll(); + } + }); + }, pollingInterval); + }; + poll(); + } + } catch (error) { + if (!isMountedRef.current) { + return; + } + + if (error.status === 401) { + message.warn(t('hooks.fetchData.loginRequired')); + } else { + message.error(t('hooks.fetchData.fetchFailed')); + } + } finally { + if (isMountedRef.current) { + Loading.hide(); + setLoading(false); + } + } + }, + [ + fetchFunc, + mapDataFunc, + clearPollingTimer, + pollingInterval, + message, + t, + pageOffset, + additionalPollingFuncs, + ] + ); + + // 开始轮询 + const startPolling = useCallback(() => { + clearPollingTimer(); + setIsPolling(true); + isPollingRef.current = true; + + const poll = () => { + pollingTimerRef.current = setTimeout(() => { + fetchData({}, true).then(() => { + if (pollingTimerRef.current && isMountedRef.current) { + poll(); + } + }); + }, pollingInterval); + }; + + poll(); + }, [pollingInterval, clearPollingTimer, fetchData]); + + // 停止轮询 + const stopPolling = useCallback(() => { + clearPollingTimer(); + setIsPolling(false); + isPollingRef.current = false; + }, [clearPollingTimer]); + + // 搜索参数变化时,自动刷新数据 + // 使用 useEffect + 深比较,而不是将对象作为依赖项 + useEffect(() => { + if (!isMountedRef.current) return; + + // 序列化当前参数 + const currentParamsString = JSON.stringify({ + keyword: searchParams.keyword, + filterType: searchParams.filter.type, + filterStatus: searchParams.filter.status, + filterTags: searchParams.filter.tags, + filterCategories: searchParams.filter.categories, + selectedStar: searchParams.filter.selectedStar, + current: searchParams.current, + pageSize: searchParams.pageSize, + }); + + // 检查参数是否真的变化了 + if (currentParamsString === prevSearchParamsRef.current) { + return; + } + prevSearchParamsRef.current = currentParamsString; + + // 防抖处理 + const timer = setTimeout(() => { + if (isMountedRef.current) { + fetchData(); + } + }, searchParams?.keyword ? 500 : 0); + + return () => { + clearTimeout(timer); + }; + }, [searchParams, fetchData]); + + // 组件卸载时清理轮询和状态 + useEffect(() => { + isMountedRef.current = true; + + if (autoRefresh) { + startPolling(); + } + + return () => { + isMountedRef.current = false; + clearPollingTimer(); + Loading.hideAll(); + }; + }, []); + + return { + loading, + tableData, + pagination: { + ...pagination, + current: searchParams.current, + pageSize: searchParams.pageSize, + }, + searchParams, + setSearchParams, + setPagination, + handleFiltersChange, + handleKeywordChange, + fetchData, + isPolling, + startPolling, + stopPolling, + }; +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index dea74fdf..98177674 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -49,9 +49,9 @@ export default defineConfig({ }; // Python 服务: rag, synthesis, annotation, evaluation, models - const pythonPaths = ["rag", "synthesis", "annotation", "knowledge-base", "data-collection", "evaluation", "models"]; + const pythonPaths = ["rag", "operators", "cleaning", "synthesis", "annotation", "knowledge-base", "data-collection", "evaluation", "models"]; // Java 服务: data-management, knowledge-base - const javaPaths = ["data-management", "operators", "cleansing"]; + const javaPaths = ["data-management"]; const proxy: Record = {}; for (const p of pythonPaths) {