Files
frontend/plans/frontend_cursor_pagination_design.md
lafay 8a0aea1c59 feat(pagination): implement cursor-based pagination across the app
Add useCursorPagination hook and update multiple screens and services
to use cursor-based pagination for better performance and consistency.

- Add useCursorPagination hook with deduplication and caching support
- Add cursor pagination types to infrastructure layer
- Refactor HomeScreen, PostDetailScreen, SearchScreen for posts/comments
- Refactor GroupMembersScreen, JoinGroupScreen for groups/members
- Refactor MessageListScreen, NotificationsScreen for messages
- Update post, message, group, comment, notification services with cursor endpoints
- Add CursorPaginationRequest/Response DTOs
- Remove deprecated OPTIMIZATION_DESIGN.md documentation
2026-03-20 23:00:27 +08:00

30 KiB
Raw Blame History

前端游标分页适配方案设计文档

一、背景说明

后端已完成游标分页功能实现,共改造了 11 个接口。前端需要对接这些新的游标分页接口,替换现有的基于页码的分页方式。

后端游标分页接口列表

模块 接口路径 排序方式
帖子 GET /posts/cursor created_at DESC
帖子搜索 GET /posts/search/cursor created_at DESC
用户帖子 GET /users/:id/posts/cursor created_at DESC
会话列表 GET /conversations/cursor updated_at DESC
消息列表 GET /conversations/:id/messages/cursor seq DESC
评论列表 GET /comments/post/:id/cursor created_at DESC
评论回复 GET /comments/:id/replies/cursor created_at ASC
通知列表 GET /notifications/cursor created_at DESC
群组列表 GET /groups/cursor created_at DESC
群成员 GET /groups/:id/members/cursor join_time DESC
群公告 GET /groups/:id/announcements/cursor created_at DESC

后端 API 规范

请求参数:

  • cursor - 游标字符串(首次请求不传)
  • direction - 分页方向:forwardbackward(默认 forward
  • page_size - 每页数量(默认 20最大 100

响应格式:

{
  "items": [...],
  "next_cursor": "xxx",
  "prev_cursor": "xxx",
  "has_more": true
}

二、现有前端分页机制分析

2.1 分页类型定义 (src/infrastructure/pagination/types.ts)

// 现有分页状态 - 基于页码
interface PaginationState {
  currentPage: number;
  pageSize: number;
  totalLoaded: number;
  hasMore: boolean;
  isLoading: boolean;
  lastLoadTime: number;
  cursor?: string | number | null;  // 已支持但未充分使用
  hasError: boolean;
  error?: string;
}

// 现有 FetchPageFunction
type FetchPageFunction<T = any> = (
  page: number,
  pageSize: number,
  cursor?: string | number | null
) => Promise<{
  data: T[];
  hasMore: boolean;
  cursor?: string | number | null;
}>;

2.2 分页状态管理器 (PaginationStateManager.ts)

  • 使用 currentPage 作为分页基准
  • 基于页码的缓存机制(pageCaches: Map<string, Map<number, PageCache>>
  • loadMore() 方法自动递增页码

2.3 usePagination Hook (usePagination.ts)

  • 暴露 currentPageloadMorerefresh 等方法
  • 内部调用 PaginationStateManager 进行状态管理
  • 已有 cursor 支持,但 UI 层仍使用页码

2.4 现有 API 服务层

postService.ts:

// 现有分页方式
async getPosts(page = 1, pageSize = 20, tab?: string): Promise<PaginatedData<Post>>

messageService.ts:

// 现有分页方式 - 基于 seq
async getMessages(conversationId, afterSeq?, beforeSeq?, limit?): Promise<MessageListResponse>

2.5 现有页面组件分页方式

各页面使用手动管理状态的方式:

const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);

const loadMore = useCallback(() => {
  if (!loading && hasMore) {
    const nextPage = page + 1;
    setPage(nextPage);
    loadData(nextPage);
  }
}, [loading, hasMore, page, loadData]);

涉及页面:

  • HomeScreen.tsx - 帖子列表
  • SearchScreen.tsx - 帖子搜索
  • PostDetailScreen.tsx - 帖子详情(评论列表)
  • MessageListScreen.tsx - 会话列表
  • ChatScreen.tsx - 消息列表
  • NotificationsScreen.tsx - 通知列表
  • GroupMembersScreen.tsx - 群组成员
  • JoinGroupScreen.tsx - 群组列表

三、游标分页类型定义设计

3.1 新增游标分页相关类型 (src/infrastructure/pagination/types.ts)

// ==================== 游标分页类型 ====================

/**
 * 游标分页方向
 */
export type CursorDirection = 'forward' | 'backward';

/**
 * 游标分页请求参数
 */
export interface CursorPageRequest {
  cursor?: string | null;
  direction?: CursorDirection;
  page_size?: number;
}

/**
 * 游标分页响应
 */
export interface CursorPageResponse<T> {
  items: T[];
  next_cursor: string | null;
  prev_cursor: string | null;
  has_more: boolean;
}

/**
 * 游标分页状态 - 扩展现有 PaginationState
 */
export interface CursorPaginationState {
  // 游标相关
  nextCursor: string | null;
  prevCursor: string | null;
  currentCursor: string | null;
  
  // 方向
  direction: CursorDirection;
  
  // 通用
  hasMore: boolean;
  isLoading: boolean;
  hasError: boolean;
  error?: string;
}

/**
 * 游标分页配置
 */
export interface CursorPaginationConfig {
  pageSize: number;
  prefetchThreshold: number;
  enablePrefetch: boolean;
  maxRetries: number;
  retryDelay: number;
}

/**
 * 游标分页加载函数
 */
export type CursorFetchFunction<T = any> = (
  request: CursorPageRequest
) => Promise<CursorPageResponse<T>>;

/**
 * 游标分页加载结果
 */
export interface CursorLoadMoreResult<T = any> {
  success: boolean;
  data: T[];
  hasMore: boolean;
  nextCursor: string | null;
  prevCursor: string | null;
  fromCache: boolean;
  error?: string;
}

/**
 * 游标分页刷新结果
 */
export interface CursorRefreshResult<T = any> {
  success: boolean;
  data: T[];
  hasMore: boolean;
  nextCursor: string | null;
  error?: string;
}

3.2 新增游标分页 DTO 类型 (src/types/dto.ts)

// ==================== 游标分页 DTO ====================

/**
 * 游标分页响应基础结构
 */
export interface CursorPaginatedResponse<T> {
  items: T[];
  next_cursor: string | null;
  prev_cursor: string | null;
  has_more: boolean;
}

/**
 * 帖子列表游标分页响应
 */
export interface PostCursorResponse extends CursorPaginatedResponse<PostDTO> {}

/**
 * 会话列表游标分页响应
 */
export interface ConversationCursorResponse extends CursorPaginatedResponse<ConversationResponse> {}

/**
 * 消息列表游标分页响应
 */
export interface MessageCursorResponse extends CursorPaginatedResponse<MessageResponse> {}

/**
 * 评论列表游标分页响应
 */
export interface CommentCursorResponse extends CursorPaginatedResponse<CommentDTO> {}

/**
 * 通知列表游标分页响应
 */
export interface NotificationCursorResponse extends CursorPaginatedResponse<NotificationDTO> {}

/**
 * 群组列表游标分页响应
 */
export interface GroupCursorResponse extends CursorPaginatedResponse<GroupResponse> {}

/**
 * 群成员列表游标分页响应
 */
export interface GroupMemberCursorResponse extends CursorPaginatedResponse<GroupMemberResponse> {}

/**
 * 群公告列表游标分页响应
 */
export interface GroupAnnouncementCursorResponse extends CursorPaginatedResponse<GroupAnnouncementResponse> {}

四、API 服务层改造设计

4.1 新增游标分页 API 方法

postService.ts 改造

class PostService {
  // ==================== 游标分页方法 ====================
  
  /**
   * 获取帖子列表(游标分页)
   * GET /api/v1/posts/cursor
   */
  async getPostsCursor(
    request: CursorPageRequest = {}
  ): Promise<CursorPageResponse<Post>> {
    const params: Record<string, any> = {
      page_size: request.page_size || 20,
    };
    if (request.cursor) params.cursor = request.cursor;
    if (request.direction) params.direction = request.direction;
    
    const response = await api.get<PostCursorResponse>('/posts/cursor', params);
    return {
      items: response.data.items,
      next_cursor: response.data.next_cursor,
      prev_cursor: response.data.prev_cursor,
      has_more: response.data.has_more,
    };
  }

  /**
   * 搜索帖子(游标分页)
   * GET /api/v1/posts/search/cursor
   */
  async searchPostsCursor(
    keyword: string,
    request: CursorPageRequest = {}
  ): Promise<CursorPageResponse<Post>> {
    const params: Record<string, any> = {
      keyword,
      page_size: request.page_size || 20,
    };
    if (request.cursor) params.cursor = request.cursor;
    if (request.direction) params.direction = request.direction;
    
    const response = await api.get<PostCursorResponse>('/posts/search/cursor', params);
    return response.data;
  }

  /**
   * 获取用户帖子列表(游标分页)
   * GET /api/v1/users/:id/posts/cursor
   */
  async getUserPostsCursor(
    userId: string,
    request: CursorPageRequest = {}
  ): Promise<CursorPageResponse<Post>> {
    const params: Record<string, any> = {
      page_size: request.page_size || 20,
    };
    if (request.cursor) params.cursor = request.cursor;
    if (request.direction) params.direction = request.direction;
    
    const response = await api.get<PostCursorResponse>(
      `/users/${userId}/posts/cursor`,
      params
    );
    return response.data;
  }
}

messageService.ts 改造

class MessageService {
  // ==================== 会话列表游标分页 ====================
  
  /**
   * 获取会话列表(游标分页)
   * GET /api/v1/conversations/cursor
   */
  async getConversationsCursor(
    request: CursorPageRequest = {}
  ): Promise<CursorPageResponse<ConversationResponse>> {
    const params: Record<string, any> = {
      page_size: request.page_size || 20,
    };
    if (request.cursor) params.cursor = request.cursor;
    if (request.direction) params.direction = request.direction;
    
    const response = await api.get<ConversationCursorResponse>('/conversations/cursor', params);
    return response.data;
  }

  // ==================== 消息列表游标分页 ====================
  
  /**
   * 获取消息列表(游标分页)
   * GET /api/v1/conversations/:id/messages/cursor
   */
  async getMessagesCursor(
    conversationId: string,
    request: CursorPageRequest = {}
  ): Promise<CursorPageResponse<MessageResponse>> {
    const params: Record<string, any> = {
      page_size: request.page_size || 20,
    };
    if (request.cursor) params.cursor = request.cursor;
    if (request.direction) params.direction = request.direction;
    
    const response = await api.get<MessageCursorResponse>(
      `/conversations/${encodeURIComponent(conversationId)}/messages/cursor`,
      params
    );
    return response.data;
  }
}

新增 commentService.ts

// 新建文件: src/services/commentService.ts

import { api } from './api';
import { CommentCursorResponse, CursorPageRequest, CursorPageResponse } from '../types/dto';
import { CommentDTO } from '../types/dto';

class CommentService {
  /**
   * 获取帖子评论列表(游标分页)
   * GET /api/v1/comments/post/:id/cursor
   */
  async getPostCommentsCursor(
    postId: string,
    request: CursorPageRequest = {}
  ): Promise<CursorPageResponse<CommentDTO>> {
    const params: Record<string, any> = {
      page_size: request.page_size || 20,
    };
    if (request.cursor) params.cursor = request.cursor;
    if (request.direction) params.direction = request.direction;
    
    const response = await api.get<CommentCursorResponse>(
      `/comments/post/${postId}/cursor`,
      params
    );
    return response.data;
  }

  /**
   * 获取评论回复列表(游标分页)
   * GET /api/v1/comments/:id/replies/cursor
   */
  async getCommentRepliesCursor(
    commentId: string,
    request: CursorPageRequest = {}
  ): Promise<CursorPageResponse<CommentDTO>> {
    const params: Record<string, any> = {
      page_size: request.page_size || 20,
    };
    if (request.cursor) params.cursor = request.cursor;
    if (request.direction) params.direction = request.direction;
    
    const response = await api.get<CommentCursorResponse>(
      `/comments/${commentId}/replies/cursor`,
      params
    );
    return response.data;
  }
}

export const commentService = new CommentService();

新增 notificationService.ts

// 新建文件: src/services/notificationService.ts

import { api } from './api';
import { NotificationCursorResponse, CursorPageRequest, CursorPageResponse } from '../types/dto';
import { NotificationDTO } from '../types/dto';

class NotificationService {
  /**
   * 获取通知列表(游标分页)
   * GET /api/v1/notifications/cursor
   */
  async getNotificationsCursor(
    request: CursorPageRequest = {}
  ): Promise<CursorPageResponse<NotificationDTO>> {
    const params: Record<string, any> = {
      page_size: request.page_size || 20,
    };
    if (request.cursor) params.cursor = request.cursor;
    if (request.direction) params.direction = request.direction;
    
    const response = await api.get<NotificationCursorResponse>('/notifications/cursor', params);
    return response.data;
  }
}

export const notificationService = new NotificationService();

新增 groupService.ts (扩展现有群组服务)

// 在现有 groupService.ts 中添加游标分页方法

class GroupService {
  /**
   * 获取群组列表(游标分页)
   * GET /api/v1/groups/cursor
   */
  async getGroupsCursor(
    request: CursorPageRequest = {}
  ): Promise<CursorPageResponse<GroupResponse>> {
    const params: Record<string, any> = {
      page_size: request.page_size || 20,
    };
    if (request.cursor) params.cursor = request.cursor;
    if (request.direction) params.direction = request.direction;
    
    const response = await api.get<GroupCursorResponse>('/groups/cursor', params);
    return response.data;
  }

  /**
   * 获取群成员列表(游标分页)
   * GET /api/v1/groups/:id/members/cursor
   */
  async getGroupMembersCursor(
    groupId: string,
    request: CursorPageRequest = {}
  ): Promise<CursorPageResponse<GroupMemberResponse>> {
    const params: Record<string, any> = {
      page_size: request.page_size || 20,
    };
    if (request.cursor) params.cursor = request.cursor;
    if (request.direction) params.direction = request.direction;
    
    const response = await api.get<GroupMemberCursorResponse>(
      `/groups/${groupId}/members/cursor`,
      params
    );
    return response.data;
  }

  /**
   * 获取群公告列表(游标分页)
   * GET /api/v1/groups/:id/announcements/cursor
   */
  async getGroupAnnouncementsCursor(
    groupId: string,
    request: CursorPageRequest = {}
  ): Promise<CursorPageResponse<GroupAnnouncementResponse>> {
    const params: Record<string, any> = {
      page_size: request.page_size || 20,
    };
    if (request.cursor) params.cursor = request.cursor;
    if (request.direction) params.direction = request.direction;
    
    const response = await api.get<GroupAnnouncementCursorResponse>(
      `/groups/${groupId}/announcements/cursor`,
      params
    );
    return response.data;
  }
}

五、分页 Hook 改造设计

5.1 新增 useCursorPagination Hook (src/hooks/useCursorPagination.ts)

/**
 * 游标分页 Hook
 * 
 * 提供基于游标的分页功能,包括:
 * - loadMore: 加载下一页
 * - loadPrevious: 加载上一页(双向游标支持)
 * - refresh: 刷新数据
 * - 状态管理
 */

import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
import { CursorPaginationState, CursorPaginationConfig } from '../infrastructure/pagination/types';

export interface UseCursorPaginationOptions<T = any> {
  /** 分页键 */
  key: string | null;
  /** 数据获取函数 */
  fetchFunction: CursorFetchFunction<T>;
  /** 分页配置 */
  config?: Partial<CursorPaginationConfig>;
  /** 初始数据 */
  initialData?: T[];
  /** 是否自动加载第一页 */
  autoLoad?: boolean;
  /** 是否启用自动预加载 */
  enableAutoPrefetch?: boolean;
}

export interface UseCursorPaginationReturn<T = any> {
  /** 当前数据列表 */
  data: T[];
  /** 是否正在加载 */
  isLoading: boolean;
  /** 是否正在刷新 */
  isRefreshing: boolean;
  /** 是否有更多数据 */
  hasMore: boolean;
  /** 是否有上一页 */
  hasPrevious: boolean;
  /** 是否有错误 */
  hasError: boolean;
  /** 错误信息 */
  error?: string;
  /** 当前游标 */
  currentCursor: string | null;
  /** 加载下一页 */
  loadMore: () => Promise<CursorLoadMoreResult<T>>;
  /** 加载上一页 */
  loadPrevious: () => Promise<CursorLoadMoreResult<T>>;
  /** 刷新数据 */
  refresh: () => Promise<CursorRefreshResult<T>>;
  /** 重置分页状态 */
  reset: () => void;
}

export function useCursorPagination<T = any>(
  options: UseCursorPaginationOptions<T>
): UseCursorPaginationReturn<T> {
  const {
    key,
    fetchFunction,
    config: userConfig,
    initialData = [],
    autoLoad = false,
    enableAutoPrefetch = true,
  } = options;

  // 合并配置
  const config = useMemo(() => ({
    ...DEFAULT_CURSOR_PAGINATION_CONFIG,
    ...userConfig,
  }), [userConfig]);

  // 数据状态
  const [data, setData] = useState<T[]>(initialData);
  const [state, setState] = useState<CursorPaginationState>({
    nextCursor: null,
    prevCursor: null,
    currentCursor: null,
    direction: 'forward',
    hasMore: true,
    isLoading: false,
    hasError: false,
    error: undefined,
  });

  // Refs
  const autoLoadRef = useRef(false);
  const isMountedRef = useRef(true);

  // 派生的 UI 状态
  const isRefreshing = state.isLoading && data.length > 0;
  const hasPrevious = state.prevCursor !== null;

  /**
   * 加载下一页
   */
  const loadMore = useCallback(async (): Promise<CursorLoadMoreResult<T>> => {
    if (!key) {
      return { success: false, data: [], hasMore: false, nextCursor: null, prevCursor: null, fromCache: false, error: 'No pagination key' };
    }

    try {
      setState(prev => ({ ...prev, isLoading: true, hasError: false }));
      
      const response = await fetchFunction({
        cursor: state.nextCursor,
        direction: 'forward',
        page_size: config.pageSize,
      });

      if (isMountedRef.current) {
        setData(prev => [...prev, ...response.items]);
        setState({
          nextCursor: response.next_cursor,
          prevCursor: response.prev_cursor,
          currentCursor: response.next_cursor,
          direction: 'forward',
          hasMore: response.has_more,
          isLoading: false,
          hasError: false,
        });
      }

      return {
        success: true,
        data: response.items,
        hasMore: response.has_more,
        nextCursor: response.next_cursor,
        prevCursor: response.prev_cursor,
        fromCache: false,
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : 'Unknown error';
      if (isMountedRef.current) {
        setState(prev => ({ ...prev, isLoading: false, hasError: true, error: errorMessage }));
      }
      return { success: false, data: [], hasMore: false, nextCursor: null, prevCursor: null, fromCache: false, error: errorMessage };
    }
  }, [key, fetchFunction, state.nextCursor, config.pageSize]);

  /**
   * 加载上一页
   */
  const loadPrevious = useCallback(async (): Promise<CursorLoadMoreResult<T>> => {
    if (!key || !state.prevCursor) {
      return { success: false, data: [], hasMore: false, nextCursor: null, prevCursor: null, fromCache: false, error: 'No previous page' };
    }

    try {
      setState(prev => ({ ...prev, isLoading: true, hasError: false }));
      
      const response = await fetchFunction({
        cursor: state.prevCursor,
        direction: 'backward',
        page_size: config.pageSize,
      });

      if (isMountedRef.current) {
        setData(prev => [...response.items, ...prev]);
        setState({
          nextCursor: response.next_cursor,
          prevCursor: response.prev_cursor,
          currentCursor: response.prev_cursor,
          direction: 'backward',
          hasMore: response.has_more,
          isLoading: false,
          hasError: false,
        });
      }

      return {
        success: true,
        data: response.items,
        hasMore: response.has_more,
        nextCursor: response.next_cursor,
        prevCursor: response.prev_cursor,
        fromCache: false,
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : 'Unknown error';
      if (isMountedRef.current) {
        setState(prev => ({ ...prev, isLoading: false, hasError: true, error: errorMessage }));
      }
      return { success: false, data: [], hasMore: false, nextCursor: null, prevCursor: null, fromCache: false, error: errorMessage };
    }
  }, [key, fetchFunction, state.prevCursor, config.pageSize]);

  /**
   * 刷新数据
   */
  const refresh = useCallback(async (): Promise<CursorRefreshResult<T>> => {
    if (!key) {
      return { success: false, data: [], hasMore: false, nextCursor: null, error: 'No pagination key' };
    }

    try {
      setState(prev => ({ ...prev, isLoading: true, hasError: false }));
      
      const response = await fetchFunction({
        cursor: null,
        direction: 'forward',
        page_size: config.pageSize,
      });

      if (isMountedRef.current) {
        setData(response.items);
        setState({
          nextCursor: response.next_cursor,
          prevCursor: null,
          currentCursor: null,
          direction: 'forward',
          hasMore: response.has_more,
          isLoading: false,
          hasError: false,
        });
      }

      return {
        success: true,
        data: response.items,
        hasMore: response.has_more,
        nextCursor: response.next_cursor,
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : 'Unknown error';
      if (isMountedRef.current) {
        setState(prev => ({ ...prev, isLoading: false, hasError: true, error: errorMessage }));
      }
      return { success: false, data: [], hasMore: false, nextCursor: null, error: errorMessage };
    }
  }, [key, fetchFunction, config.pageSize]);

  /**
   * 重置分页状态
   */
  const reset = useCallback(() => {
    if (isMountedRef.current) {
      setData([]);
      setState({
        nextCursor: null,
        prevCursor: null,
        currentCursor: null,
        direction: 'forward',
        hasMore: true,
        isLoading: false,
        hasError: false,
      });
      autoLoadRef.current = false;
    }
  }, []);

  // 自动加载第一页
  useEffect(() => {
    if (!key || !autoLoad || autoLoadRef.current) return;
    if (data.length > 0) return;

    autoLoadRef.current = true;
    refresh();
  }, [key, autoLoad, data.length, refresh]);

  // 组件卸载时清理
  useEffect(() => {
    return () => {
      isMountedRef.current = false;
    };
  }, []);

  return {
    data,
    isLoading: state.isLoading,
    isRefreshing,
    hasMore: state.hasMore,
    hasPrevious,
    hasError: state.hasError,
    error: state.error,
    currentCursor: state.currentCursor,
    loadMore,
    loadPrevious,
    refresh,
    reset,
  };
}

六、各列表页面改造计划

6.1 帖子列表 (HomeScreen.tsx)

// 改造后的分页方式
const {
  data: posts,
  isLoading,
  hasMore,
  loadMore,
  refresh,
} = useCursorPagination<Post>({
  key: 'home-posts',
  fetchFunction: async (request) => {
    return postService.getPostsCursor(request);
  },
  config: { pageSize: 20 },
  autoLoad: true,
});

// UI 层使用 FlatList/FlashList 的 onEndReached
<FlatList
  data={posts}
  onEndReached={loadMore}
  onEndReachedThreshold={0.3}
  refreshing={isLoading && posts.length > 0}
  onRefresh={refresh}
/>

6.2 会话列表 (MessageListScreen.tsx)

const {
  data: conversations,
  isLoading,
  hasMore,
  loadMore,
  refresh,
} = useCursorPagination<ConversationResponse>({
  key: 'conversations',
  fetchFunction: async (request) => {
    return messageService.getConversationsCursor(request);
  },
  autoLoad: true,
});

6.3 消息列表 (ChatScreen.tsx)

// 聊天消息需要支持双向加载
const {
  data: messages,
  isLoading,
  hasMore,
  hasPrevious,
  loadMore,      // 加载更多历史
  loadPrevious,  // 加载更新的消息
  refresh,
} = useCursorPagination<MessageResponse>({
  key: `chat-${conversationId}`,
  fetchFunction: async (request) => {
    return messageService.getMessagesCursor(conversationId, request);
  },
});

6.4 帖子搜索 (SearchScreen.tsx)

const {
  data: searchResults,
  isLoading,
  hasMore,
  loadMore,
  refresh,
} = useCursorPagination<Post>({
  key: `search-${keyword}`,
  fetchFunction: async (request) => {
    return postService.searchPostsCursor(keyword, request);
  },
  autoLoad: !!keyword,
});

6.5 评论列表 (PostDetailScreen.tsx)

const {
  data: comments,
  isLoading,
  hasMore,
  loadMore,
  refresh,
} = useCursorPagination<CommentDTO>({
  key: `comments-${postId}`,
  fetchFunction: async (request) => {
    return commentService.getPostCommentsCursor(postId, request);
  },
});

6.6 通知列表 (NotificationsScreen.tsx)

const {
  data: notifications,
  isLoading,
  hasMore,
  loadMore,
  refresh,
} = useCursorPagination<NotificationDTO>({
  key: 'notifications',
  fetchFunction: async (request) => {
    return notificationService.getNotificationsCursor(request);
  },
});

6.7 群组列表 (JoinGroupScreen.tsx)

const {
  data: groups,
  isLoading,
  hasMore,
  loadMore,
  refresh,
} = useCursorPagination<GroupResponse>({
  key: 'groups',
  fetchFunction: async (request) => {
    return groupService.getGroupsCursor(request);
  },
});

6.8 群组成员 (GroupMembersScreen.tsx)

const {
  data: members,
  isLoading,
  hasMore,
  loadMore,
  refresh,
} = useCursorPagination<GroupMemberResponse>({
  key: `group-members-${groupId}`,
  fetchFunction: async (request) => {
    return groupService.getGroupMembersCursor(groupId, request);
  },
});

七、需要修改的文件清单

7.1 新增文件

文件路径 说明
src/hooks/useCursorPagination.ts 新增游标分页 Hook
src/services/commentService.ts 新增评论服务(含游标分页)
src/services/notificationService.ts 新增通知服务(含游标分页)
src/types/cursor.ts 新增游标分页相关类型定义

7.2 需要修改的文件

文件路径 修改内容
src/types/dto.ts 新增游标分页 DTO 类型
src/services/postService.ts 新增游标分页方法(保留旧方法兼容)
src/services/messageService.ts 新增会话/消息游标分页方法
src/services/groupService.ts 新增群组相关游标分页方法
src/infrastructure/pagination/types.ts 新增游标分页类型定义
src/infrastructure/pagination/CursorPaginationStateManager.ts 新增游标分页状态管理器(可选)
src/screens/home/HomeScreen.tsx 改造为使用 useCursorPagination
src/screens/home/SearchScreen.tsx 改造为使用 useCursorPagination
src/screens/home/PostDetailScreen.tsx 改造评论列表使用 useCursorPagination
src/screens/message/MessageListScreen.tsx 改造为使用 useCursorPagination
src/screens/message/ChatScreen.tsx 改造消息列表使用 useCursorPagination
src/screens/message/NotificationsScreen.tsx 改造为使用 useCursorPagination
src/screens/message/JoinGroupScreen.tsx 改造为使用 useCursorPagination
src/screens/message/GroupMembersScreen.tsx 改造为使用 useCursorPagination

八、实现顺序建议

阶段一:基础设施(优先级高)

  1. src/types/dto.ts 新增游标分页 DTO 类型
  2. src/infrastructure/pagination/types.ts 新增游标分页类型
  3. 创建 src/hooks/useCursorPagination.ts

阶段二API 服务层

  1. 改造 postService.ts - 新增游标分页方法
  2. 改造 messageService.ts - 新增游标分页方法
  3. 创建 commentService.ts
  4. 创建 notificationService.ts
  5. 改造 groupService.ts - 新增游标分页方法

阶段三UI 组件改造(按依赖顺序)

  1. HomeScreen.tsx - 帖子列表
  2. SearchScreen.tsx - 帖子搜索
  3. PostDetailScreen.tsx - 评论列表
  4. MessageListScreen.tsx - 会话列表
  5. ChatScreen.tsx - 消息列表
  6. NotificationsScreen.tsx - 通知列表
  7. JoinGroupScreen.tsx - 群组列表
  8. GroupMembersScreen.tsx - 群组成员

九、架构图

graph TB
    subgraph "UI Layer"
        HomeScreen[HomeScreen.tsx]
        SearchScreen[SearchScreen.tsx]
        ChatScreen[ChatScreen.tsx]
        MessageListScreen[MessageListScreen.tsx]
        NotificationsScreen[NotificationsScreen.tsx]
        GroupMembersScreen[GroupMembersScreen.tsx]
    end

    subgraph "Hooks Layer"
        useCursorPagination[useCursorPagination.ts]
        usePagination[usePagination.ts - 保留兼容]
    end

    subgraph "Service Layer"
        postService[postService.ts]
        messageService[messageService.ts]
        commentService[commentService.ts]
        notificationService[notificationService.ts]
        groupService[groupService.ts]
    end

    subgraph "API Layer"
        API[API Client]
    end

    subgraph "Backend"
        CursorAPI[游标分页 API]
    end

    HomeScreen --> useCursorPagination
    SearchScreen --> useCursorPagination
    ChatScreen --> useCursorPagination
    MessageListScreen --> useCursorPagination
    NotificationsScreen --> useCursorPagination
    GroupMembersScreen --> useCursorPagination

    useCursorPagination --> postService
    useCursorPagination --> messageService
    useCursorPagination --> commentService
    useCursorPagination --> notificationService
    useCursorPagination --> groupService

    postService --> API
    messageService --> API
    commentService --> API
    notificationService --> API
    groupService --> API

    API --> CursorAPI

十、注意事项

10.1 兼容性考虑

  • 保留现有的页码分页方法一段时间,避免一次性全量替换带来的风险
  • 通过功能开关或配置控制是否启用游标分页

10.2 消息列表特殊处理

  • 聊天消息需要支持双向加载(加载历史 + 加载最新)
  • 需要处理好新消息的插入位置(头部 vs 尾部)

10.3 缓存策略

  • 游标分页的缓存策略需要调整,不再基于页码
  • 可以考虑基于 cursor 字符串进行缓存

10.4 错误处理

  • 网络错误时需要显示重试选项
  • 游标失效时需要提示用户刷新列表