From 97c7762f2b7e34c1a34f29850caf7f090c1f5c61 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Sat, 21 Mar 2026 01:36:40 +0800 Subject: [PATCH] feat(navigation): add profile stack navigator and fix API endpoints - Add ProfileStackNavigatorComponent with full navigation stack (Profile, Settings, EditProfile, AccountSecurity, MyPosts, Bookmarks, NotificationSettings, BlockedUsers) - Add null checks for response data in authService login, register, and refreshToken methods - Fix postService API routes to use correct endpoints (/posts instead of /posts/cursor) - Remove completed cursor pagination design document --- .dockerignore | 2 +- plans/frontend_cursor_pagination_design.md | 1131 ------------------- src/navigation/SimpleMobileTabNavigator.tsx | 93 +- src/services/authService.ts | 9 + src/services/postService.ts | 36 +- 5 files changed, 118 insertions(+), 1153 deletions(-) delete mode 100644 plans/frontend_cursor_pagination_design.md diff --git a/.dockerignore b/.dockerignore index 7ffff64..eb1e6ac 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,6 @@ dist dist-web dist-android dist-android-update.zip -android +android screenshots *.log diff --git a/plans/frontend_cursor_pagination_design.md b/plans/frontend_cursor_pagination_design.md deleted file mode 100644 index 00268ed..0000000 --- a/plans/frontend_cursor_pagination_design.md +++ /dev/null @@ -1,1131 +0,0 @@ -# 前端游标分页适配方案设计文档 - -## 一、背景说明 - -后端已完成游标分页功能实现,共改造了 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 = ( - page: number, - pageSize: number, - cursor?: string | number | null -) => Promise<{ - data: T[]; - hasMore: boolean; - cursor?: string | number | null; -}>; -``` - -### 2.2 分页状态管理器 (`PaginationStateManager.ts`) - -- 使用 `currentPage` 作为分页基准 -- 基于页码的缓存机制(`pageCaches: Map>`) -- `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> -``` - -**messageService.ts:** -```typescript -// 现有分页方式 - 基于 seq -async getMessages(conversationId, afterSeq?, beforeSeq?, limit?): Promise -``` - -### 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 { - 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 = ( - request: CursorPageRequest -) => Promise>; - -/** - * 游标分页加载结果 - */ -export interface CursorLoadMoreResult { - success: boolean; - data: T[]; - hasMore: boolean; - nextCursor: string | null; - prevCursor: string | null; - fromCache: boolean; - error?: string; -} - -/** - * 游标分页刷新结果 - */ -export interface CursorRefreshResult { - success: boolean; - data: T[]; - hasMore: boolean; - nextCursor: string | null; - error?: string; -} -``` - -### 3.2 新增游标分页 DTO 类型 (`src/types/dto.ts`) - -```typescript -// ==================== 游标分页 DTO ==================== - -/** - * 游标分页响应基础结构 - */ -export interface CursorPaginatedResponse { - items: T[]; - next_cursor: string | null; - prev_cursor: string | null; - has_more: boolean; -} - -/** - * 帖子列表游标分页响应 - */ -export interface PostCursorResponse extends CursorPaginatedResponse {} - -/** - * 会话列表游标分页响应 - */ -export interface ConversationCursorResponse extends CursorPaginatedResponse {} - -/** - * 消息列表游标分页响应 - */ -export interface MessageCursorResponse extends CursorPaginatedResponse {} - -/** - * 评论列表游标分页响应 - */ -export interface CommentCursorResponse extends CursorPaginatedResponse {} - -/** - * 通知列表游标分页响应 - */ -export interface NotificationCursorResponse extends CursorPaginatedResponse {} - -/** - * 群组列表游标分页响应 - */ -export interface GroupCursorResponse extends CursorPaginatedResponse {} - -/** - * 群成员列表游标分页响应 - */ -export interface GroupMemberCursorResponse extends CursorPaginatedResponse {} - -/** - * 群公告列表游标分页响应 - */ -export interface GroupAnnouncementCursorResponse extends CursorPaginatedResponse {} -``` - ---- - -## 四、API 服务层改造设计 - -### 4.1 新增游标分页 API 方法 - -#### postService.ts 改造 -```typescript -class PostService { - // ==================== 游标分页方法 ==================== - - /** - * 获取帖子列表(游标分页) - * GET /api/v1/posts/cursor - */ - async getPostsCursor( - request: CursorPageRequest = {} - ): Promise> { - const params: Record = { - 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('/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> { - const params: Record = { - 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('/posts/search/cursor', params); - return response.data; - } - - /** - * 获取用户帖子列表(游标分页) - * GET /api/v1/users/:id/posts/cursor - */ - async getUserPostsCursor( - userId: string, - request: CursorPageRequest = {} - ): Promise> { - const params: Record = { - 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( - `/users/${userId}/posts/cursor`, - params - ); - return response.data; - } -} -``` - -#### messageService.ts 改造 -```typescript -class MessageService { - // ==================== 会话列表游标分页 ==================== - - /** - * 获取会话列表(游标分页) - * GET /api/v1/conversations/cursor - */ - async getConversationsCursor( - request: CursorPageRequest = {} - ): Promise> { - const params: Record = { - 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('/conversations/cursor', params); - return response.data; - } - - // ==================== 消息列表游标分页 ==================== - - /** - * 获取消息列表(游标分页) - * GET /api/v1/conversations/:id/messages/cursor - */ - async getMessagesCursor( - conversationId: string, - request: CursorPageRequest = {} - ): Promise> { - const params: Record = { - 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( - `/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> { - const params: Record = { - 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( - `/comments/post/${postId}/cursor`, - params - ); - return response.data; - } - - /** - * 获取评论回复列表(游标分页) - * GET /api/v1/comments/:id/replies/cursor - */ - async getCommentRepliesCursor( - commentId: string, - request: CursorPageRequest = {} - ): Promise> { - const params: Record = { - 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( - `/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> { - const params: Record = { - 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('/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> { - const params: Record = { - 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('/groups/cursor', params); - return response.data; - } - - /** - * 获取群成员列表(游标分页) - * GET /api/v1/groups/:id/members/cursor - */ - async getGroupMembersCursor( - groupId: string, - request: CursorPageRequest = {} - ): Promise> { - const params: Record = { - 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( - `/groups/${groupId}/members/cursor`, - params - ); - return response.data; - } - - /** - * 获取群公告列表(游标分页) - * GET /api/v1/groups/:id/announcements/cursor - */ - async getGroupAnnouncementsCursor( - groupId: string, - request: CursorPageRequest = {} - ): Promise> { - const params: Record = { - 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( - `/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 { - /** 分页键 */ - key: string | null; - /** 数据获取函数 */ - fetchFunction: CursorFetchFunction; - /** 分页配置 */ - config?: Partial; - /** 初始数据 */ - initialData?: T[]; - /** 是否自动加载第一页 */ - autoLoad?: boolean; - /** 是否启用自动预加载 */ - enableAutoPrefetch?: boolean; -} - -export interface UseCursorPaginationReturn { - /** 当前数据列表 */ - data: T[]; - /** 是否正在加载 */ - isLoading: boolean; - /** 是否正在刷新 */ - isRefreshing: boolean; - /** 是否有更多数据 */ - hasMore: boolean; - /** 是否有上一页 */ - hasPrevious: boolean; - /** 是否有错误 */ - hasError: boolean; - /** 错误信息 */ - error?: string; - /** 当前游标 */ - currentCursor: string | null; - /** 加载下一页 */ - loadMore: () => Promise>; - /** 加载上一页 */ - loadPrevious: () => Promise>; - /** 刷新数据 */ - refresh: () => Promise>; - /** 重置分页状态 */ - reset: () => void; -} - -export function useCursorPagination( - options: UseCursorPaginationOptions -): UseCursorPaginationReturn { - const { - key, - fetchFunction, - config: userConfig, - initialData = [], - autoLoad = false, - enableAutoPrefetch = true, - } = options; - - // 合并配置 - const config = useMemo(() => ({ - ...DEFAULT_CURSOR_PAGINATION_CONFIG, - ...userConfig, - }), [userConfig]); - - // 数据状态 - const [data, setData] = useState(initialData); - const [state, setState] = useState({ - 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> => { - 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> => { - 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> => { - 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({ - key: 'home-posts', - fetchFunction: async (request) => { - return postService.getPostsCursor(request); - }, - config: { pageSize: 20 }, - autoLoad: true, -}); - -// UI 层使用 FlatList/FlashList 的 onEndReached - 0} - onRefresh={refresh} -/> -``` - -### 6.2 会话列表 (MessageListScreen.tsx) - -```typescript -const { - data: conversations, - isLoading, - hasMore, - loadMore, - refresh, -} = useCursorPagination({ - 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({ - key: `chat-${conversationId}`, - fetchFunction: async (request) => { - return messageService.getMessagesCursor(conversationId, request); - }, -}); -``` - -### 6.4 帖子搜索 (SearchScreen.tsx) - -```typescript -const { - data: searchResults, - isLoading, - hasMore, - loadMore, - refresh, -} = useCursorPagination({ - 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({ - key: `comments-${postId}`, - fetchFunction: async (request) => { - return commentService.getPostCommentsCursor(postId, request); - }, -}); -``` - -### 6.6 通知列表 (NotificationsScreen.tsx) - -```typescript -const { - data: notifications, - isLoading, - hasMore, - loadMore, - refresh, -} = useCursorPagination({ - key: 'notifications', - fetchFunction: async (request) => { - return notificationService.getNotificationsCursor(request); - }, -}); -``` - -### 6.7 群组列表 (JoinGroupScreen.tsx) - -```typescript -const { - data: groups, - isLoading, - hasMore, - loadMore, - refresh, -} = useCursorPagination({ - key: 'groups', - fetchFunction: async (request) => { - return groupService.getGroupsCursor(request); - }, -}); -``` - -### 6.8 群组成员 (GroupMembersScreen.tsx) - -```typescript -const { - data: members, - isLoading, - hasMore, - loadMore, - refresh, -} = useCursorPagination({ - 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 错误处理 -- 网络错误时需要显示重试选项 -- 游标失效时需要提示用户刷新列表 diff --git a/src/navigation/SimpleMobileTabNavigator.tsx b/src/navigation/SimpleMobileTabNavigator.tsx index 1591417..c50c951 100644 --- a/src/navigation/SimpleMobileTabNavigator.tsx +++ b/src/navigation/SimpleMobileTabNavigator.tsx @@ -40,8 +40,21 @@ export type ScheduleStackParamList = { CourseDetail: { course: any; relatedCourses?: any[] }; }; +// Profile Stack 类型定义 +export type ProfileStackParamList = { + Profile: undefined; + Settings: undefined; + EditProfile: undefined; + AccountSecurity: undefined; + MyPosts: undefined; + Bookmarks: undefined; + NotificationSettings: undefined; + BlockedUsers: undefined; +}; + // ==================== Stack Navigators ==================== const ScheduleStack = createNativeStackNavigator(); +const ProfileStack = createNativeStackNavigator(); // ==================== 常量 ==================== const TAB_BAR_HEIGHT = 64; @@ -80,6 +93,84 @@ function ScheduleStackNavigatorComponent() { ); } +/** + * Profile Stack Navigator 组件 + */ +function ProfileStackNavigatorComponent() { + return ( + + + + + + + + + + + ); +} + /** * 主组件:SimpleMobileTabNavigator */ @@ -108,7 +199,7 @@ export function SimpleMobileTabNavigator() { case 'ScheduleTab': return ; case 'ProfileTab': - return ; + return ; default: return ; } diff --git a/src/services/authService.ts b/src/services/authService.ts index fa28a9d..c32f953 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -150,6 +150,9 @@ class AuthService { async login(data: LoginRequest): Promise { const response = await api.post('/auth/login', data); + if (!response.data) { + throw new Error('登录响应数据为空'); + } if (response.data.token) { await api.setToken(response.data.token); } @@ -165,6 +168,9 @@ class AuthService { async register(data: RegisterRequest): Promise { const response = await api.post('/auth/register', data); + if (!response.data) { + throw new Error('注册响应数据为空'); + } if (response.data.token) { await api.setToken(response.data.token); } @@ -233,6 +239,9 @@ class AuthService { try { const response = await api.post('/auth/refresh'); + if (!response.data) { + return null; + } if (response.data.token) { await api.setToken(response.data.token); } diff --git a/src/services/postService.ts b/src/services/postService.ts index f4ece35..72afa79 100644 --- a/src/services/postService.ts +++ b/src/services/postService.ts @@ -287,19 +287,17 @@ class PostService { /** * 获取帖子列表(游标分页) - * GET /api/v1/posts/cursor + * GET /api/v1/posts * @param params 游标分页请求参数(包含 post_type 可选:recommend, follow, hot, latest) */ - async getPostsCursor( - params: CursorPaginationRequest = {} - ): Promise> { - try { - const response = await api.get>('/posts/cursor', { - params: { - cursor: params.cursor, - page_size: params.page_size, - post_type: params.post_type, - }, + async getPostsCursor( + params: CursorPaginationRequest = {} + ): Promise> { + try { + const response = await api.get>('/posts', { + cursor: params.cursor, + page_size: params.page_size, + post_type: params.post_type, }); return response.data; } catch (error) { @@ -315,7 +313,7 @@ class PostService { /** * 搜索帖子(游标分页) - * GET /api/v1/posts/search/cursor + * GET /api/v1/posts/search * @param query 搜索关键词 * @param params 游标分页请求参数 */ @@ -324,11 +322,9 @@ class PostService { params: CursorPaginationRequest = {} ): Promise> { try { - const response = await api.get>('/posts/search/cursor', { - params: { - ...params, - keyword: query, - }, + const response = await api.get>('/posts/search', { + ...params, + keyword: query, }); return response.data; } catch (error) { @@ -344,7 +340,7 @@ class PostService { /** * 获取用户帖子列表(游标分页) - * GET /api/v1/users/:id/posts/cursor + * GET /api/v1/users/:id/posts * @param userId 用户ID * @param params 游标分页请求参数 */ @@ -354,8 +350,8 @@ class PostService { ): Promise> { try { const response = await api.get>( - `/users/${userId}/posts/cursor`, - { params } + `/users/${userId}/posts`, + params ); return response.data; } catch (error) {