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

1132 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 前端游标分页适配方案设计文档
## 一、背景说明
后端已完成游标分页功能实现,共改造了 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` - 分页方向:`forward``backward`(默认 forward
- `page_size` - 每页数量(默认 20最大 100
**响应格式:**
```json
{
"items": [...],
"next_cursor": "xxx",
"prev_cursor": "xxx",
"has_more": true
}
```
---
## 二、现有前端分页机制分析
### 2.1 分页类型定义 (`src/infrastructure/pagination/types.ts`)
```typescript
// 现有分页状态 - 基于页码
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`)
- 暴露 `currentPage``loadMore``refresh` 等方法
- 内部调用 `PaginationStateManager` 进行状态管理
- 已有 `cursor` 支持,但 UI 层仍使用页码
### 2.4 现有 API 服务层
**postService.ts:**
```typescript
// 现有分页方式
async getPosts(page = 1, pageSize = 20, tab?: string): Promise<PaginatedData<Post>>
```
**messageService.ts:**
```typescript
// 现有分页方式 - 基于 seq
async getMessages(conversationId, afterSeq?, beforeSeq?, limit?): Promise<MessageListResponse>
```
### 2.5 现有页面组件分页方式
各页面使用**手动管理状态**的方式:
```typescript
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`)
```typescript
// ==================== 游标分页类型 ====================
/**
* 游标分页方向
*/
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`)
```typescript
// ==================== 游标分页 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 改造
```typescript
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 改造
```typescript
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
```typescript
// 新建文件: 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
```typescript
// 新建文件: 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 (扩展现有群组服务)
```typescript
// 在现有 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`)
```typescript
/**
* 游标分页 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)
```typescript
// 改造后的分页方式
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)
```typescript
const {
data: conversations,
isLoading,
hasMore,
loadMore,
refresh,
} = useCursorPagination<ConversationResponse>({
key: 'conversations',
fetchFunction: async (request) => {
return messageService.getConversationsCursor(request);
},
autoLoad: true,
});
```
### 6.3 消息列表 (ChatScreen.tsx)
```typescript
// 聊天消息需要支持双向加载
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)
```typescript
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)
```typescript
const {
data: comments,
isLoading,
hasMore,
loadMore,
refresh,
} = useCursorPagination<CommentDTO>({
key: `comments-${postId}`,
fetchFunction: async (request) => {
return commentService.getPostCommentsCursor(postId, request);
},
});
```
### 6.6 通知列表 (NotificationsScreen.tsx)
```typescript
const {
data: notifications,
isLoading,
hasMore,
loadMore,
refresh,
} = useCursorPagination<NotificationDTO>({
key: 'notifications',
fetchFunction: async (request) => {
return notificationService.getNotificationsCursor(request);
},
});
```
### 6.7 群组列表 (JoinGroupScreen.tsx)
```typescript
const {
data: groups,
isLoading,
hasMore,
loadMore,
refresh,
} = useCursorPagination<GroupResponse>({
key: 'groups',
fetchFunction: async (request) => {
return groupService.getGroupsCursor(request);
},
});
```
### 6.8 群组成员 (GroupMembersScreen.tsx)
```typescript
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 服务层
4. 改造 `postService.ts` - 新增游标分页方法
5. 改造 `messageService.ts` - 新增游标分页方法
6. 创建 `commentService.ts`
7. 创建 `notificationService.ts`
8. 改造 `groupService.ts` - 新增游标分页方法
### 阶段三UI 组件改造(按依赖顺序)
9. `HomeScreen.tsx` - 帖子列表
10. `SearchScreen.tsx` - 帖子搜索
11. `PostDetailScreen.tsx` - 评论列表
12. `MessageListScreen.tsx` - 会话列表
13. `ChatScreen.tsx` - 消息列表
14. `NotificationsScreen.tsx` - 通知列表
15. `JoinGroupScreen.tsx` - 群组列表
16. `GroupMembersScreen.tsx` - 群组成员
---
## 九、架构图
```mermaid
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 错误处理
- 网络错误时需要显示重试选项
- 游标失效时需要提示用户刷新列表