KeepHQ项目中的分页重置问题分析与解决方案

KeepHQ项目中的分页重置问题分析与解决方案

【免费下载链接】keep The open-source alerts management and automation platform 【免费下载链接】keep 项目地址: https://gitcode.com/GitHub_Trending/kee/keep

问题背景

在KeepHQ这个开源AIOps(人工智能运维)和警报管理平台中,分页功能是用户界面的核心组件之一。然而,在实际使用过程中,用户经常会遇到分页状态意外重置的问题,这严重影响了用户体验和操作效率。

典型场景

当用户在警报表格中进行以下操作时,分页状态会意外重置:

  1. 过滤条件变化:修改搜索条件或过滤器时
  2. 排序操作:改变表格排序方式时
  3. 数据刷新:手动刷新数据时
  4. 标签切换:在不同预设标签间切换时

问题根因分析

技术架构概述

KeepHQ采用现代化的技术栈:

  • 前端:React + TypeScript + Next.js
  • 状态管理:React Hooks + Zustand
  • 表格组件:TanStack Table (原React Table)
  • 分页逻辑:自定义分页组件

核心问题定位

通过代码分析,发现分页重置问题主要源于以下几个方面:

1. 状态管理不一致性
// 问题代码示例
const [paginationState, setPaginationState] = useState<PaginationState>({
  limit: rowStyle == "relaxed" ? 20 : 50,
  offset: 0,
});

// 当filterCel或searchCel变化时,强制重置offset为0
useEffect(() => {
  setPaginationState({
    ...paginationStateRef.current,
    offset: 0,  // 这里导致分页重置
  });
}, [filterCel, searchCel, setPaginationState]);
2. 组件间状态同步缺失

mermaid

3. URL参数与状态不同步
// URL参数变化时未正确处理分页状态
const searchParams = useSearchParams();
useEffect(() => {
  // 参数变化时未考虑分页状态的保持
  const lastAlertsCount = searchParams.get("createIncidentsFromLastAlerts");
  // ...其他参数处理,但缺少分页状态同步
}, [searchParams]);

解决方案设计

方案一:智能分页状态保持

实现原理
interface SmartPaginationState extends PaginationState {
  preserveOnFilter: boolean;
  preserveOnSearch: boolean;
  lastFilterHash?: string;
  lastSearchHash?: string;
}

// 智能分页状态管理Hook
function useSmartPagination(initialState: PaginationState) {
  const [state, setState] = useState<SmartPaginationState>({
    ...initialState,
    preserveOnFilter: true,
    preserveOnSearch: false,
  });
  
  const filterHashRef = useRef<string>('');
  const searchHashRef = useRef<string>('');

  const setSmartPagination = useCallback((newState: Partial<SmartPaginationState>) => {
    setState(prev => ({ ...prev, ...newState }));
  }, []);

  // 智能重置逻辑
  const smartReset = useCallback((trigger: 'filter' | 'search', newHash: string) => {
    setState(prev => {
      const shouldPreserve = trigger === 'filter' ? prev.preserveOnFilter : prev.preserveOnSearch;
      const prevHash = trigger === 'filter' ? filterHashRef.current : searchHashRef.current;
      
      if (shouldPreserve && prevHash === newHash) {
        return prev; // 相同条件不重置
      }
      
      // 更新hash引用
      if (trigger === 'filter') {
        filterHashRef.current = newHash;
      } else {
        searchHashRef.current = newHash;
      }
      
      return { ...prev, offset: 0 }; // 不同条件重置
    });
  }, []);

  return { state, setSmartPagination, smartReset };
}

方案二:URL同步分页状态

实现方案
// 分页状态与URL同步Hook
function useSyncedPagination() {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();
  
  const [paginationState, setPaginationState] = useState(() => {
    // 从URL参数初始化分页状态
    const page = parseInt(searchParams.get('page') || '1');
    const size = parseInt(searchParams.get('size') || '20');
    return {
      limit: size,
      offset: (page - 1) * size,
    };
  });

  // 分页状态变化时更新URL
  useEffect(() => {
    const newSearchParams = new URLSearchParams(searchParams);
    const currentPage = Math.floor(paginationState.offset / paginationState.limit) + 1;
    
    newSearchParams.set('page', currentPage.toString());
    newSearchParams.set('size', paginationState.limit.toString());
    
    router.replace(`${pathname}?${newSearchParams.toString()}`, { scroll: false });
  }, [paginationState, pathname, router, searchParams]);

  return [paginationState, setPaginationState] as const;
}

方案三:分页状态持久化

本地存储集成
interface PaginationPreset {
  presetId: string;
  pageSize: number;
  currentPage: number;
  lastUpdated: number;
}

function usePersistedPagination(presetId: string) {
  const storageKey = `pagination-${presetId}`;
  
  const [state, setState] = useState<PaginationState>(() => {
    const saved = localStorage.getItem(storageKey);
    if (saved) {
      try {
        return JSON.parse(saved);
      } catch {
        // 解析失败使用默认值
      }
    }
    return { limit: 20, offset: 0 };
  });

  // 状态变化时持久化
  useEffect(() => {
    localStorage.setItem(storageKey, JSON.stringify(state));
  }, [state, storageKey]);

  // 清理过期的持久化数据
  useEffect(() => {
    const cleanupOldPaginationData = () => {
      const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
      Object.keys(localStorage).forEach(key => {
        if (key.startsWith('pagination-')) {
          try {
            const data = JSON.parse(localStorage.getItem(key)!);
            if (data.lastUpdated && data.lastUpdated < oneWeekAgo) {
              localStorage.removeItem(key);
            }
          } catch {
            localStorage.removeItem(key);
          }
        }
      });
    };
    
    cleanupOldPaginationData();
  }, []);

  return [state, setState] as const;
}

实施路线图

阶段一:紧急修复(1-2天)

  1. 修复强制重置逻辑

    // 修改前的代码
    useEffect(() => {
      setPaginationState({ ...paginationStateRef.current, offset: 0 });
    }, [filterCel, searchCel]);
    
    // 修改后的代码
    useEffect(() => {
      // 只有过滤条件真正变化时才重置
      if (filterCel !== prevFilterCelRef.current || searchCel !== prevSearchCelRef.current) {
        setPaginationState({ ...paginationStateRef.current, offset: 0 });
        prevFilterCelRef.current = filterCel;
        prevSearchCelRef.current = searchCel;
      }
    }, [filterCel, searchCel]);
    
  2. 添加状态变化日志

    useEffect(() => {
      console.log('Pagination state changed:', {
        limit: paginationState.limit,
        offset: paginationState.offset,
        trigger: 'filter/search change'
      });
    }, [paginationState]);
    

阶段二:架构优化(3-5天)

  1. 统一状态管理

    // 创建分页状态管理Context
    const PaginationContext = createContext<PaginationContextType>(null!);
    
    export function PaginationProvider({ children }: { children: React.ReactNode }) {
      const [state, setState] = useState<PaginationState>({ limit: 20, offset: 0 });
      const value = useMemo(() => ({ state, setState }), [state]);
    
      return (
        <PaginationContext.Provider value={value}>
          {children}
        </PaginationContext.Provider>
      );
    }
    
  2. 实现智能重置策略

    const shouldResetPagination = (
      currentFilter: string | null, 
      previousFilter: string | null,
      currentSearch: string | null,
      previousSearch: string | null
    ): boolean => {
      // 定义重置策略
      const filterChanged = currentFilter !== previousFilter;
      const searchChanged = currentSearch !== previousSearch;
    
      // 只有重大条件变化才重置
      return (filterChanged && currentFilter !== null) || 
             (searchChanged && currentSearch !== null && currentSearch.length > 2);
    };
    

阶段三:用户体验增强(5-7天)

  1. 添加分页状态指示器

    function PaginationStatus({ state, totalCount }: { state: PaginationState; totalCount: number }) {
      const currentPage = Math.floor(state.offset / state.limit) + 1;
      const totalPages = Math.ceil(totalCount / state.limit);
    
      return (
        <div className="pagination-status">
          <span>Page {currentPage} of {totalPages}</span>
          <span>Showing {state.offset + 1}-{Math.min(state.offset + state.limit, totalCount)} of {totalCount} items</span>
        </div>
      );
    }
    
  2. 实现分页记忆功能

    interface PaginationMemory {
      [presetId: string]: {
        limit: number;
        lastPage: number;
        timestamp: number;
      };
    }
    
    const PAGINATION_MEMORY_EXPIRY = 30 * 60 * 1000; // 30分钟
    
    function usePaginationMemory(presetId: string) {
      const memory = useRef<PaginationMemory>({});
    
      const savePagination = useCallback((limit: number, offset: number) => {
        memory.current[presetId] = {
          limit,
          lastPage: Math.floor(offset / limit) + 1,
          timestamp: Date.now()
        };
      }, [presetId]);
    
      const getPagination = useCallback(() => {
        const saved = memory.current[presetId];
        if (saved && Date.now() - saved.timestamp < PAGINATION_MEMORY_EXPIRY) {
          return {
            limit: saved.limit,
            offset: (saved.lastPage - 1) * saved.limit
          };
        }
        return null;
      }, [presetId]);
    
      return { savePagination, getPagination };
    }
    

测试策略

单元测试覆盖

describe('Pagination Logic', () => {
  test('should not reset pagination on identical filter changes', () => {
    const initialState = { limit: 20, offset: 40 };
    const result = shouldResetPagination('status==open', 'status==open', null, null);
    expect(result).toBe(false);
  });
  
  test('should reset pagination on significant filter changes', () => {
    const result = shouldResetPagination('status==closed', 'status==open', null, null);
    expect(result).toBe(true);
  });
  
  test('should not reset pagination on minor search changes', () => {
    const result = shouldResetPagination(null, null, 'a', 'b');
    expect(result).toBe(false);
  });
});

集成测试场景

describe('Pagination Integration', () => {
  test('preserves pagination state when switching between similar filters', async () => {
    // 模拟用户操作流程
    await user.click(filterButton);
    await user.type(filterInput, 'status==open');
    await user.click(applyFilter);
    
    // 检查分页状态
    expect(paginationState.offset).toBe(0); // 首次应用过滤器应重置
    
    // 切换到第二页
    await user.click(nextPageButton);
    expect(paginationState.offset).toBe(20);
    
    // 轻微修改过滤器
    await user.clear(filterInput);
    await user.type(filterInput, 'status==open&&severity==high');
    await user.click(applyFilter);
    
    // 分页状态应保持
    expect(paginationState.offset).toBe(20);
  });
});

性能考量

内存优化

// 使用WeakMap进行分页状态存储,避免内存泄漏
const paginationStateCache = new WeakMap<object, PaginationState>();

function getPaginationState(componentInstance: object): PaginationState {
  if (!paginationStateCache.has(componentInstance)) {
    paginationStateCache.set(componentInstance, { limit: 20, offset: 0 });
  }
  return paginationStateCache.get(componentInstance)!;
}

渲染性能

// 使用useMemo优化分页计算
const paginationInfo = useMemo(() => {
  const currentPage = Math.floor(paginationState.offset / paginationState.limit) + 1;
  const totalPages = Math.ceil(totalCount / paginationState.limit);
  const startItem = paginationState.offset + 1;
  const endItem = Math.min(paginationState.offset + paginationState.limit, totalCount);
  
  return { currentPage, totalPages, startItem, endItem };
}, [paginationState, totalCount]);

总结与最佳实践

通过系统性的分析和解决方案设计,KeepHQ项目的分页重置问题可以得到有效解决。关键要点包括:

  1. 状态管理一致性:确保分页状态在组件间的正确同步
  2. 智能重置策略:根据操作类型决定是否重置分页
  3. 用户体验优先:保持用户操作习惯,减少意外状态变化
  4. 性能优化:合理使用缓存和记忆化技术

实施这些解决方案后,用户将获得更加稳定和 predictable(可预测)的分页体验,大大提升平台的整体可用性和用户满意度。

【免费下载链接】keep The open-source alerts management and automation platform 【免费下载链接】keep 项目地址: https://gitcode.com/GitHub_Trending/kee/keep

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值