1132 lines
30 KiB
Markdown
1132 lines
30 KiB
Markdown
|
|
# 前端游标分页适配方案设计文档
|
|||
|
|
|
|||
|
|
## 一、背景说明
|
|||
|
|
|
|||
|
|
后端已完成游标分页功能实现,共改造了 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 错误处理
|
|||
|
|
- 网络错误时需要显示重试选项
|
|||
|
|
- 游标失效时需要提示用户刷新列表
|