Files
frontend/plans/frontend_cursor_pagination_design.md

1132 lines
30 KiB
Markdown
Raw Normal View 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` - 分页方向:`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 错误处理
- 网络错误时需要显示重试选项
- 游标失效时需要提示用户刷新列表