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) {