diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 33ba30e..54648a5 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -314,8 +314,8 @@ jobs: labels: ${{ steps.meta.outputs.labels }} platforms: linux/amd64 provenance: false - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=registry,ref=code.littlelan.cn/carrot_bbs/frontend-web:buildcache + cache-to: type=registry,ref=code.littlelan.cn/carrot_bbs/frontend-web:buildcache,mode=max - name: Show image tags run: | diff --git a/src/core/usecases/ProcessPostUseCase.ts b/src/core/usecases/ProcessPostUseCase.ts index 1a6d775..c87bffa 100644 --- a/src/core/usecases/ProcessPostUseCase.ts +++ b/src/core/usecases/ProcessPostUseCase.ts @@ -64,10 +64,14 @@ export interface PostsState { hasMore: boolean; /** 错误信息 */ error: string | null; - /** 当前游标 */ + /** 当前游标(cursor 模式) */ cursor: string | null; + /** 当前页码(page 模式) */ + currentPage: number; /** 最后加载时间 */ lastLoadTime: number | null; + /** 上次请求的参数(用于加载更多和刷新) */ + lastParams?: GetPostsParams; } /** @@ -198,6 +202,7 @@ class ProcessPostUseCase { hasMore: true, error: null, cursor: null, + currentPage: 1, lastLoadTime: null, }; this.postsState.set(key, state); @@ -257,29 +262,32 @@ class ProcessPostUseCase { * @param key 列表键(用于区分不同的列表) */ async fetchPosts(params?: GetPostsParams, key: string = 'default'): Promise { - const state = this.getPostsState(key); - // 更新加载状态 this.updatePostsState(key, { isLoading: true, error: null }); try { - // 合并分页参数 + // 默认传递空字符串 cursor 来启动 cursor 模式 + // 注意:cursor 要放在 ...params 后面,这样只有在 params 没有传 cursor 时才使用默认值 const queryParams: GetPostsParams = { - page: params?.page || 1, pageSize: params?.pageSize || DEFAULT_PAGE_SIZE, - cursor: params?.cursor || state.cursor || undefined, ...params, + cursor: params?.cursor !== undefined ? params.cursor : '', // 默认使用空字符串启动 cursor 模式 }; const result = await postRepository.getPosts(queryParams); - // 更新状态 + // 判断是否为 cursor 模式(有 next_cursor 返回) + const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null; + + // 更新状态(保存请求参数以便加载更多时使用) this.updatePostsState(key, { posts: result.posts, hasMore: result.hasMore, - cursor: result.nextCursor || null, + cursor: isCursorMode ? result.nextCursor : null, // cursor 模式保存 cursor,否则设为 null 表示 page 模式 + currentPage: isCursorMode ? undefined : 1, // page 模式从第 1 页开始 isLoading: false, lastLoadTime: Date.now(), + lastParams: params, // 保存请求参数 }); // 通知订阅者 @@ -312,20 +320,29 @@ class ProcessPostUseCase { * @param key 列表键 */ async refreshPosts(key: string = 'default'): Promise { + const state = this.getPostsState(key); + // 更新刷新状态 this.updatePostsState(key, { isRefreshing: true, error: null }); try { + // 使用保存的请求参数(如 post_type)进行刷新 + // 默认传递空字符串 cursor 来启动 cursor 模式 const result = await postRepository.getPosts({ - page: 1, pageSize: DEFAULT_PAGE_SIZE, + ...state.lastParams, + cursor: state.lastParams?.cursor !== undefined ? state.lastParams.cursor : '', // 默认使用空字符串启动 cursor 模式 }); + // 判断是否为 cursor 模式(有 next_cursor 返回) + const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null; + // 更新状态 this.updatePostsState(key, { posts: result.posts, hasMore: result.hasMore, - cursor: result.nextCursor || null, + cursor: isCursorMode ? result.nextCursor : null, + currentPage: isCursorMode ? undefined : 1, isRefreshing: false, lastLoadTime: Date.now(), }); @@ -372,21 +389,45 @@ class ProcessPostUseCase { this.updatePostsState(key, { isLoading: true, error: null }); try { - const queryParams: GetPostsParams = { - pageSize: DEFAULT_PAGE_SIZE, - cursor: state.cursor || undefined, - }; + // 判断使用哪种分页模式:cursor 不为 null 表示使用 cursor 模式 + const useCursorMode = state.cursor !== null && state.cursor !== undefined; + + // 必须先展开 lastParams,再由本次分页字段覆盖。 + // lastParams 来自首次 fetch(通常含 page:1),若写在展开之前会把 nextPage/cursor 覆盖掉,导致永远请求第 1 页、无法加载第 3 页及以后。 + const base = { ...(state.lastParams ?? {}) } as GetPostsParams; + let queryParams: GetPostsParams; + + if (useCursorMode) { + delete base.page; + queryParams = { + ...base, + pageSize: DEFAULT_PAGE_SIZE, + cursor: state.cursor!, + }; + } else { + const nextPage = (state.currentPage || 1) + 1; + delete base.cursor; + queryParams = { + ...base, + pageSize: DEFAULT_PAGE_SIZE, + page: nextPage, + }; + } const result = await postRepository.getPosts(queryParams); // 合并新数据 const newPosts = [...state.posts, ...result.posts]; + // 判断响应是否为 cursor 模式(有 next_cursor 返回) + const responseIsCursorMode = result.nextCursor !== undefined && result.nextCursor !== null; + // 更新状态 this.updatePostsState(key, { posts: newPosts, hasMore: result.hasMore, - cursor: result.nextCursor || null, + cursor: responseIsCursorMode ? result.nextCursor : null, + currentPage: useCursorMode ? state.currentPage : ((state.currentPage || 1) + 1), isLoading: false, lastLoadTime: Date.now(), }); @@ -398,6 +439,7 @@ class ProcessPostUseCase { }; } catch (error) { const errorMessage = error instanceof Error ? error.message : '加载更多帖子失败'; + console.error('[ProcessPostUseCase] Load more error:', errorMessage); this.updatePostsState(key, { isLoading: false, error: errorMessage, diff --git a/src/data/repositories/PostRepository.ts b/src/data/repositories/PostRepository.ts index c013194..4841a11 100644 --- a/src/data/repositories/PostRepository.ts +++ b/src/data/repositories/PostRepository.ts @@ -231,9 +231,12 @@ export class PostRepository implements IPostRepository { try { const queryParams: Record = {}; - if (params?.page) queryParams.page = params.page; + if (params?.page !== undefined) queryParams.page = params.page; if (params?.pageSize) queryParams.page_size = params.pageSize; - if (params?.cursor) queryParams.cursor = params.cursor; + // 支持 cursor 参数:空字符串也传递,用于启动 cursor 模式 + if (params?.cursor !== undefined && params?.cursor !== null) { + queryParams.cursor = params.cursor; + } if (params?.post_type) queryParams.tab = params.post_type; if (params?.communityId) queryParams.community_id = params.communityId; if (params?.authorId) queryParams.author_id = params.authorId; @@ -253,9 +256,16 @@ export class PostRepository implements IPostRepository { this.saveToLocalCache(post); }); + // 计算 hasMore:优先使用 has_more,否则通过 page/total_pages 计算 + let hasMore = response.has_more; + if (hasMore === undefined && response.page !== undefined && response.total_pages !== undefined) { + hasMore = response.page < response.total_pages; + } + hasMore = hasMore ?? false; + return { posts, - hasMore: response.has_more ?? false, + hasMore, nextCursor: response.next_cursor, total: response.total, }; diff --git a/src/hooks/useCursorPagination.ts b/src/hooks/useCursorPagination.ts index 9ba0a56..1fa6d3c 100644 --- a/src/hooks/useCursorPagination.ts +++ b/src/hooks/useCursorPagination.ts @@ -29,7 +29,7 @@ export function useCursorPagination( const effectivePageSize = Math.min(Math.max(1, pageSize), MAX_PAGE_SIZE); const [state, setState] = useState>({ - items: [], + list: [], nextCursor: null, prevCursor: null, hasMore: true, // 初始设为 true,以便首次加载可以执行 @@ -86,7 +86,7 @@ export function useCursorPagination( setState(prev => ({ ...prev, - items: [...prev.items, ...response.items], + list: [...prev.list, ...response.list], nextCursor: response.next_cursor, prevCursor: response.prev_cursor, hasMore: response.has_more && response.next_cursor !== null, @@ -134,7 +134,7 @@ export function useCursorPagination( setState(prev => ({ ...prev, // 向前加载时,新数据放在前面 - items: [...response.items, ...prev.items], + list: [...response.list, ...prev.list], nextCursor: response.next_cursor, prevCursor: response.prev_cursor, hasMore: response.has_more, @@ -169,7 +169,7 @@ export function useCursorPagination( if (cancelledRef.current) return; setState({ - items: response.items, + list: response.list, nextCursor: response.next_cursor, prevCursor: response.prev_cursor, hasMore: response.has_more && response.next_cursor !== null, @@ -196,7 +196,7 @@ export function useCursorPagination( isFirstLoadRef.current = true; isLoadingRef.current = false; setState({ - items: [], + list: [], nextCursor: null, prevCursor: null, hasMore: true, // 重置后也设为 true,以便可以重新加载 @@ -208,16 +208,16 @@ export function useCursorPagination( }, []); // 设置数据(用于外部数据注入) - const setItems = useCallback( + const setList = useCallback( ( - items: T[], + list: T[], nextCursor: string | null, prevCursor: string | null, hasMore: boolean ) => { setState(prev => ({ ...prev, - items, + list, nextCursor, prevCursor, hasMore, @@ -230,7 +230,7 @@ export function useCursorPagination( // 自动加载第一页 useEffect(() => { // 使用 ref 防止重复加载,不依赖 loadMore 函数引用 - // 只检查 autoLoad 和 autoLoadRef,不再依赖 state.items.length + // 只检查 autoLoad 和 autoLoadRef,不再依赖 state.list.length if (!autoLoad || autoLoadRef.current) return; autoLoadRef.current = true; @@ -261,7 +261,7 @@ export function useCursorPagination( setState(prev => ({ ...prev, - items: response.items, + list: response.list, nextCursor: response.next_cursor, prevCursor: response.prev_cursor, hasMore: response.has_more && response.next_cursor !== null, @@ -296,7 +296,7 @@ export function useCursorPagination( loadPrevious, refresh, reset, - setItems, + setList, }; } diff --git a/src/hooks/useDifferentialPosts.ts b/src/hooks/useDifferentialPosts.ts index c441c9d..d10150b 100644 --- a/src/hooks/useDifferentialPosts.ts +++ b/src/hooks/useDifferentialPosts.ts @@ -4,13 +4,12 @@ * 集成 ProcessPostUseCase 进行状态管理 */ -import { useState, useRef, useCallback, useEffect, useMemo } from 'react'; +import { useState, useRef, useCallback, useEffect } from 'react'; import { PostIdentifier, PostUpdate, PostUpdateType, PostDiffConfig, - PostDiffResult, } from '../infrastructure/diff/postTypes'; import { PostDiffCalculator, @@ -119,7 +118,6 @@ export function useDifferentialPosts( enableDiff = true, enableBatching = true, maxPostCount = 10000, - changeRatioThreshold = 0.5, listKey = 'default', autoSubscribe = true, } = options; @@ -147,37 +145,7 @@ export function useDifferentialPosts( const previousPostsRef = useRef(initialPosts); const unsubscribeRef = useRef<(() => void) | null>(null); - // ==================== 初始化 ==================== - - // 初始化差异计算器 - useEffect(() => { - if (enableDiff) { - calculatorRef.current = createPostDiffCalculator(diffConfig); - } - return () => { - calculatorRef.current = null; - }; - }, [enableDiff, diffConfig]); - - // 初始化批量处理器 - useEffect(() => { - if (enableBatching) { - batcherRef.current = createPostUpdateBatcher(batcherOptions); - - // 订阅批量更新 - const unsubscribe = batcherRef.current.subscribe((updates) => { - handleBatchUpdates(updates); - }); - - return () => { - unsubscribe(); - batcherRef.current?.destroy(); - batcherRef.current = null; - }; - } - }, [enableBatching, batcherOptions]); - - // ==================== 批量更新处理 ==================== + // ==================== 批量更新处理(须在订阅 batcher 的 useEffect 之前定义)==================== /** * 处理批量更新 @@ -289,123 +257,34 @@ export function useDifferentialPosts( } }, []); - // ==================== 差异计算 ==================== + // ==================== 初始化 ==================== - /** - * 计算并应用差异 - */ - const calculateAndApplyDiff = useCallback((newPosts: T[]) => { - const previousPosts = previousPostsRef.current; - - // 如果帖子数量变化太大,直接重置 - const previousCount = previousPosts.length; - const currentCount = newPosts.length; - const changeRatio = previousCount === 0 ? 1 : Math.abs(currentCount - previousCount) / previousCount; - - if (changeRatio > changeRatioThreshold) { - setPosts(newPosts); - previousPostsRef.current = newPosts; - return; + // 初始化差异计算器 + useEffect(() => { + if (enableDiff) { + calculatorRef.current = createPostDiffCalculator(diffConfig); } + return () => { + calculatorRef.current = null; + }; + }, [enableDiff, diffConfig]); - // 使用差异计算 - if (enableDiff && calculatorRef.current) { - const diff = calculatorRef.current.calculateDiff(previousPosts, newPosts); + // 初始化批量处理器 + useEffect(() => { + if (!enableBatching) return; - if (diff.shouldReset) { - setPosts(newPosts); - } else { - // 将差异转换为更新操作 - const updates: PostUpdate[] = []; + batcherRef.current = createPostUpdateBatcher(batcherOptions); - // 处理新增 - if (diff.added.length > 0) { - if (diff.added.length === 1) { - updates.push({ - type: PostUpdateType.ADD, - post: diff.added[0], - timestamp: Date.now(), - }); - } else { - updates.push({ - type: PostUpdateType.BATCH_ADD, - posts: diff.added, - timestamp: Date.now(), - }); - } - } + const unsubscribe = batcherRef.current.subscribe((updates) => { + handleBatchUpdates(updates); + }); - // 处理更新 - if (diff.updated.length > 0) { - if (diff.updated.length === 1) { - updates.push({ - type: PostUpdateType.UPDATE, - postId: diff.updated[0].post.id, - updates: diff.updated[0].changes, - timestamp: Date.now(), - }); - } else { - updates.push({ - type: PostUpdateType.BATCH_UPDATE, - updates: diff.updated.map(u => ({ - postId: u.post.id, - changes: u.changes, - })), - timestamp: Date.now(), - }); - } - } - - // 处理删除 - if (diff.deleted.length > 0) { - if (diff.deleted.length === 1) { - updates.push({ - type: PostUpdateType.DELETE, - postId: diff.deleted[0].post.id, - timestamp: Date.now(), - }); - } else { - updates.push({ - type: PostUpdateType.BATCH_DELETE, - postIds: diff.deleted.map(d => d.post.id), - timestamp: Date.now(), - }); - } - } - - // 处理移动 - if (diff.moved.length > 0) { - for (const move of diff.moved) { - updates.push({ - type: PostUpdateType.MOVE, - postId: move.post.id, - toIndex: move.toIndex, - timestamp: Date.now(), - }); - } - } - - // 应用更新 - if (updates.length > 0) { - if (enableBatching && batcherRef.current) { - batcherRef.current.addUpdates(updates); - } else { - handleBatchUpdates(updates); - } - } - } - } else { - // 不使用差异计算,直接设置 - setPosts(newPosts); - } - - // 更新帖子数量限制检查 - if (newPosts.length > maxPostCount) { - console.warn(`[useDifferentialPosts] Post count (${newPosts.length}) exceeds limit (${maxPostCount})`); - } - - previousPostsRef.current = newPosts; - }, [enableDiff, enableBatching, maxPostCount, changeRatioThreshold, handleBatchUpdates]); + return () => { + unsubscribe(); + batcherRef.current?.destroy(); + batcherRef.current = null; + }; + }, [enableBatching, batcherOptions, handleBatchUpdates]); // ==================== UseCase 订阅 ==================== @@ -415,26 +294,39 @@ export function useDifferentialPosts( const handleUseCaseEvent = useCallback((event: PostUseCaseEvent) => { switch (event.type) { case 'state_changed': { - const { state } = event.payload as { key: string; state: PostsState }; + const { key: eventKey, state } = event.payload as { key: string; state: PostsState }; + // 全局订阅:只应用当前列表 key,避免其他 Tab/列表的 state 覆盖本列表(如 createPost 会更新所有 key) + if (eventKey !== listKey) { + break; + } if (state) { setLoading(state.isLoading); setRefreshing(state.isRefreshing); setError(state.error); setHasMore(state.hasMore); - - // 如果帖子列表有变化,进行差异计算 + + // ProcessPostUseCase 的 state.posts 已是合并后的完整列表。 + // 不再走差异计算 + PostUpdateBatcher:批量应用晚于 previousPostsRef 更新时, + // BATCH_ADD 会在过短的 currentPosts 上 splice,导致连续游标加载「请求成功但列表不增长」。 + batcherRef.current?.clearPending(); + calculatorRef.current?.reset(); if (state.posts && state.posts.length > 0) { - calculateAndApplyDiff(state.posts as unknown as T[]); + const next = state.posts as unknown as T[]; + if (next.length > maxPostCount) { + console.warn(`[useDifferentialPosts] Post count (${next.length}) exceeds limit (${maxPostCount})`); + } + setPosts(next); + previousPostsRef.current = next; + } else { + setPosts([]); + previousPostsRef.current = []; } } break; } case 'posts_loaded': { - const { posts: loadedPosts } = event.payload; - if (loadedPosts) { - calculateAndApplyDiff(loadedPosts as T[]); - } + // fetchPosts 在 updatePostsState 时已发出 state_changed(含合并后 posts),此处不再应用,避免与其它 key 竞态或重复计算 break; } @@ -485,12 +377,17 @@ export function useDifferentialPosts( } case 'error_occurred': { - const { error: errorMsg } = event.payload; - setError(errorMsg); + const payload = event.payload as { key?: string; error?: string }; + if (payload.key !== undefined && payload.key !== listKey) { + break; + } + if (payload.error !== undefined) { + setError(payload.error); + } break; } } - }, [calculateAndApplyDiff]); + }, [listKey, maxPostCount]); // 订阅 UseCase 状态变化 useEffect(() => { diff --git a/src/infrastructure/pagination/types.ts b/src/infrastructure/pagination/types.ts index f07466a..f2e7798 100644 --- a/src/infrastructure/pagination/types.ts +++ b/src/infrastructure/pagination/types.ts @@ -205,7 +205,7 @@ export interface CursorPaginationConfig { */ export interface CursorPaginationState { /** 数据项列表 */ - items: T[]; + list: T[]; /** 下一页游标 */ nextCursor: string | null; /** 上一页游标 */ @@ -235,7 +235,7 @@ export interface CursorPaginationActions { /** 重置状态 */ reset: () => void; /** 设置数据(用于外部数据注入) */ - setItems: (items: T[], nextCursor: string | null, prevCursor: string | null, hasMore: boolean) => void; + setList: (list: T[], nextCursor: string | null, prevCursor: string | null, hasMore: boolean) => void; } /** @@ -253,7 +253,7 @@ export type CursorFetchFunction = (params: { /** 额外参数 */ extraParams?: P; }) => Promise<{ - items: T[]; + list: T[]; next_cursor: string | null; prev_cursor: string | null; has_more: boolean; diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index 68e3ac6..bf672c0 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -88,6 +88,10 @@ export const HomeScreen: React.FC = () => { // 用于跟踪当前页面显示的帖子 ID,以便从 store 同步状态 const postIdsRef = useRef>(new Set()); const lastSwipeAtRef = useRef(0); + + // 网格模式滚动位置检测 + const gridScrollYRef = useRef(0); + const isLoadingMoreRef = useRef(false); // 构建一个以 postId 为 key 的 map,用于快速查找 const postsMap = useMemo(() => { @@ -131,14 +135,30 @@ export const HomeScreen: React.FC = () => { // 加载更多方法 const loadMore = useCallback(async () => { - if (hasMore && !isLoading) { + if (hasMore && !isLoading && !isLoadingMoreRef.current) { + isLoadingMoreRef.current = true; try { await processPostUseCase.loadMorePosts(listKey); } catch (err) { console.error('加载更多失败:', err); + } finally { + isLoadingMoreRef.current = false; } } }, [listKey, hasMore, isLoading]); + + // 网格模式滚动处理 - 检测是否滚动到底部 + const handleGridScroll = useCallback((event: any) => { + const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent; + const scrollY = contentOffset.y; + const visibleHeight = layoutMeasurement.height; + const contentHeight = contentSize.height; + + // 当滚动到距离底部 200px 时触发加载更多 + if (scrollY + visibleHeight >= contentHeight - 200) { + loadMore(); + } + }, [loadMore]); // 刷新方法 - 先获取正确类型的帖子,再刷新 const refresh = useCallback(async () => { @@ -485,6 +505,7 @@ export const HomeScreen: React.FC = () => { ]} showsVerticalScrollIndicator={false} scrollEventThrottle={100} + onScroll={handleGridScroll} refreshControl={ { } > {distributePostsToColumns.map((column, index) => renderWaterfallColumn(column, index))} + {/* 加载更多指示器 */} + {isLoading && displayPosts.length > 0 && ( + + + + )} ); } @@ -767,4 +794,9 @@ const styles = StyleSheet.create({ height: 72, borderRadius: 36, }, + loadingMore: { + paddingVertical: 20, + alignItems: 'center', + justifyContent: 'center', + }, }); diff --git a/src/screens/home/PostDetailScreen.tsx b/src/screens/home/PostDetailScreen.tsx index 6b9b10d..ad5e071 100644 --- a/src/screens/home/PostDetailScreen.tsx +++ b/src/screens/home/PostDetailScreen.tsx @@ -72,7 +72,7 @@ export const PostDetailScreen: React.FC = () => { // 使用游标分页 Hook 管理评论列表 const { - items: paginatedComments, + list: paginatedComments, isLoading: isCommentsLoading, isRefreshing: isCommentsRefreshing, hasMore: hasMoreComments, @@ -206,13 +206,13 @@ export const PostDetailScreen: React.FC = () => { // 如果是从评论按钮跳转过来的,加载完成后滚动到评论区 useEffect(() => { - if (shouldScrollToComments && !loading && comments.length > 0) { + if (shouldScrollToComments && !loading && (comments?.length ?? 0) > 0) { // 延迟确保列表已完成渲染和布局 setTimeout(() => { flatListRef.current?.scrollToIndex({ index: 0, animated: true, viewPosition: 0 }); }, 500); } - }, [shouldScrollToComments, loading, comments.length]); + }, [shouldScrollToComments, loading, comments?.length]); // 动态设置导航头部 useEffect(() => { @@ -1413,7 +1413,7 @@ export const PostDetailScreen: React.FC = () => { 加载更多评论 - ) : comments.length > 0 ? ( + ) : (comments?.length ?? 0) > 0 ? ( 没有更多评论了 @@ -1546,7 +1546,7 @@ export const PostDetailScreen: React.FC = () => { 加载更多评论 - ) : comments.length > 0 ? ( + ) : (comments?.length ?? 0) > 0 ? ( 没有更多评论了 diff --git a/src/screens/home/SearchScreen.tsx b/src/screens/home/SearchScreen.tsx index 0e728a4..a00ad87 100644 --- a/src/screens/home/SearchScreen.tsx +++ b/src/screens/home/SearchScreen.tsx @@ -83,7 +83,7 @@ export const SearchScreen: React.FC = ({ onBack, navigation: // 使用游标分页进行帖子搜索 const { - items: searchResults, + list: searchResults, isLoading, isRefreshing, hasMore, @@ -93,7 +93,7 @@ export const SearchScreen: React.FC = ({ onBack, navigation: } = useCursorPagination( async ({ cursor, pageSize, extraParams }) => { if (!extraParams?.query) { - return { items: [], next_cursor: null, prev_cursor: null, has_more: false }; + return { list: [], next_cursor: null, prev_cursor: null, has_more: false }; } const response = await postService.searchPostsCursor(extraParams.query, { cursor, diff --git a/src/screens/message/GroupMembersScreen.tsx b/src/screens/message/GroupMembersScreen.tsx index ffb281b..6f7b189 100644 --- a/src/screens/message/GroupMembersScreen.tsx +++ b/src/screens/message/GroupMembersScreen.tsx @@ -72,7 +72,7 @@ const GroupMembersScreen: React.FC = () => { // 使用游标分页 Hook 管理成员列表 const { - items: members, + list: members, isLoading, isRefreshing, hasMore, diff --git a/src/screens/message/JoinGroupScreen.tsx b/src/screens/message/JoinGroupScreen.tsx index e6df582..36125a9 100644 --- a/src/screens/message/JoinGroupScreen.tsx +++ b/src/screens/message/JoinGroupScreen.tsx @@ -36,7 +36,7 @@ const JoinGroupScreen: React.FC = () => { // 使用游标分页 Hook 管理群组列表 const { - items: groups, + list: groups, isLoading, isRefreshing, hasMore, diff --git a/src/screens/message/MessageListScreen.tsx b/src/screens/message/MessageListScreen.tsx index f63b0de..efc2e2b 100644 --- a/src/screens/message/MessageListScreen.tsx +++ b/src/screens/message/MessageListScreen.tsx @@ -164,7 +164,7 @@ export const MessageListScreen: React.FC = () => { // 【游标分页】使用 useCursorPagination hook 获取会话列表 const { - items: conversations, + list: conversationList, isLoading, isRefreshing, hasMore, @@ -453,7 +453,7 @@ export const MessageListScreen: React.FC = () => { if (activeSearchTab === 'chat') { const results: SearchResultItem[] = []; - for (const conv of conversations) { + for (const conv of conversationList) { await messageManager.fetchMessages(String(conv.id)); const messages = messageManager.getMessages(String(conv.id)); const matchedMessages = messages.filter(msg => { @@ -484,7 +484,7 @@ export const MessageListScreen: React.FC = () => { } finally { setIsSearching(false); } - }, [conversations, activeSearchTab]); + }, [conversationList, activeSearchTab]); // 搜索文本变化时触发搜索 useEffect(() => { @@ -533,7 +533,7 @@ export const MessageListScreen: React.FC = () => { // 合并列表数据 const listData: ConversationResponse[] = [ createSystemMessageItem(), - ...conversations, + ...conversationList, ]; // 总未读数 @@ -923,7 +923,7 @@ export const MessageListScreen: React.FC = () => { - {isLoading && conversations.length === 0 ? ( + {isLoading && conversationList.length === 0 ? ( diff --git a/src/screens/message/NotificationsScreen.tsx b/src/screens/message/NotificationsScreen.tsx index c239f5f..a17161c 100644 --- a/src/screens/message/NotificationsScreen.tsx +++ b/src/screens/message/NotificationsScreen.tsx @@ -88,7 +88,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack ); const { - items: messages, + list: messages, isLoading, isRefreshing, hasMore, diff --git a/src/services/commentService.ts b/src/services/commentService.ts index 71989f2..4049ec5 100644 --- a/src/services/commentService.ts +++ b/src/services/commentService.ts @@ -255,15 +255,18 @@ class CommentService { params: CursorPaginationRequest = {} ): Promise> { try { - const response = await api.get>( - `/comments/post/${postId}/cursor`, - { params } - ); - return response.data; + const response = await api.get(`/comments/post/${postId}/cursor`, { params }); + const raw = response.data; + return { + list: Array.isArray(raw?.list) ? raw.list : [], + next_cursor: raw?.next_cursor ?? null, + prev_cursor: raw?.prev_cursor ?? null, + has_more: raw?.has_more ?? false, + }; } catch (error) { console.error('获取帖子评论列表失败:', error); return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, @@ -282,15 +285,18 @@ class CommentService { params: CursorPaginationRequest = {} ): Promise> { try { - const response = await api.get>( - `/comments/${commentId}/replies/cursor`, - { params } - ); - return response.data; + const response = await api.get(`/comments/${commentId}/replies/cursor`, { params }); + const raw = response.data; + return { + list: Array.isArray(raw?.list) ? raw.list : [], + next_cursor: raw?.next_cursor ?? null, + prev_cursor: raw?.prev_cursor ?? null, + has_more: raw?.has_more ?? false, + }; } catch (error) { console.error('获取评论回复列表失败:', error); return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, diff --git a/src/services/groupService.ts b/src/services/groupService.ts index f936644..f6ca233 100644 --- a/src/services/groupService.ts +++ b/src/services/groupService.ts @@ -341,7 +341,7 @@ class GroupService { } catch (error) { console.error('获取群组列表失败:', error); return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, @@ -368,7 +368,7 @@ class GroupService { } catch (error) { console.error('获取群组成员列表失败:', error); return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, @@ -395,7 +395,7 @@ class GroupService { } catch (error) { console.error('获取群公告列表失败:', error); return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, diff --git a/src/services/messageService.ts b/src/services/messageService.ts index 844e878..1bcab13 100644 --- a/src/services/messageService.ts +++ b/src/services/messageService.ts @@ -573,11 +573,17 @@ class MessageService { '/conversations/cursor', { params } ); - return response.data; + const raw = response.data; + return { + list: Array.isArray(raw?.list) ? raw.list : [], + next_cursor: raw?.next_cursor ?? null, + prev_cursor: raw?.prev_cursor ?? null, + has_more: raw?.has_more ?? false, + }; } catch (error) { console.error('获取会话列表失败:', error); return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, @@ -596,15 +602,21 @@ class MessageService { params: CursorPaginationRequest = {} ): Promise> { try { - const response = await api.get>( + const response = await api.get( `/conversations/${encodeURIComponent(conversationId)}/messages/cursor`, { params } ); - return response.data; + const raw = response.data; + return { + list: Array.isArray(raw?.list) ? raw.list : [], + next_cursor: raw?.next_cursor ?? null, + prev_cursor: raw?.prev_cursor ?? null, + has_more: raw?.has_more ?? false, + }; } catch (error) { console.error('获取消息列表失败:', error); return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, @@ -631,32 +643,36 @@ class MessageService { const data = response.data; - // 兼容两种 API 响应格式 + // 游标或带 page/total_pages 的分页(统一为 CursorPaginationResponse:list) if (data && typeof data === 'object') { - // 游标分页格式 - if (Array.isArray(data.items)) { + const list = Array.isArray(data.list) ? data.list : null; + if (list) { + const isPageShape = + typeof data.page === 'number' && + typeof data.total_pages === 'number' && + data.next_cursor === undefined && + data.prev_cursor === undefined; + if (isPageShape) { + const currentPage = data.page || 1; + const totalPages = data.total_pages || 1; + return { + list, + next_cursor: currentPage < totalPages ? String(currentPage + 1) : null, + prev_cursor: currentPage > 1 ? String(currentPage - 1) : null, + has_more: currentPage < totalPages, + }; + } return { - items: data.items, + list, next_cursor: data.next_cursor ?? null, prev_cursor: data.prev_cursor ?? null, has_more: data.has_more ?? false, }; } - // 传统分页格式 - 转换为游标格式 - if (Array.isArray(data.list)) { - const currentPage = data.page || 1; - const totalPages = data.total_pages || 1; - return { - items: data.list, - next_cursor: currentPage < totalPages ? String(currentPage + 1) : null, - prev_cursor: currentPage > 1 ? String(currentPage - 1) : null, - has_more: currentPage < totalPages, - }; - } } - + return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, @@ -664,7 +680,7 @@ class MessageService { } catch (error) { console.error('获取系统消息列表失败:', error); return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index f808b70..b5de12e 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -172,7 +172,7 @@ class NotificationService { } catch (error) { console.error('获取通知列表失败:', error); return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, diff --git a/src/services/postService.ts b/src/services/postService.ts index c3675c4..9445c60 100644 --- a/src/services/postService.ts +++ b/src/services/postService.ts @@ -304,37 +304,33 @@ class PostService { const response = await api.get('/posts', requestParams); const data = response.data; - - // 兼容两种 API 响应格式: - // 1. 游标分页格式:{ items, next_cursor, prev_cursor, has_more } - // 2. 传统分页格式:{ list, total, page, page_size, total_pages } - if (data && typeof data === 'object') { - // 游标分页格式 - if (Array.isArray(data.items)) { - return { - items: data.items, - next_cursor: data.next_cursor ?? null, - prev_cursor: data.prev_cursor ?? null, - has_more: data.has_more ?? false, - }; - } - // 传统分页格式 - 转换为游标格式 - if (Array.isArray(data.list)) { - const currentPage = data.page || 1; - const totalPages = data.total_pages || 1; - return { - items: data.list, - // 使用页码作为游标 - next_cursor: currentPage < totalPages ? String(currentPage + 1) : null, - prev_cursor: currentPage > 1 ? String(currentPage - 1) : null, - has_more: currentPage < totalPages, - }; - } - } - - // 默认返回空数据 + + if (data && typeof data === 'object' && Array.isArray(data.list)) { + const isPageShape = + typeof data.page === 'number' && + typeof data.total_pages === 'number' && + data.next_cursor === undefined && + data.prev_cursor === undefined; + if (isPageShape) { + const currentPage = data.page || 1; + const totalPages = data.total_pages || 1; + return { + list: data.list, + next_cursor: currentPage < totalPages ? String(currentPage + 1) : null, + prev_cursor: currentPage > 1 ? String(currentPage - 1) : null, + has_more: currentPage < totalPages, + }; + } + return { + list: data.list, + next_cursor: data.next_cursor ?? null, + prev_cursor: data.prev_cursor ?? null, + has_more: data.has_more ?? false, + }; + } + return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, @@ -342,7 +338,7 @@ class PostService { } catch (error) { console.error('获取帖子列表失败:', error); return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, @@ -367,31 +363,33 @@ class PostService { }); const data = response.data; - - // 兼容两种 API 响应格式 - if (data && typeof data === 'object') { - if (Array.isArray(data.items)) { - return { - items: data.items, - next_cursor: data.next_cursor ?? null, - prev_cursor: data.prev_cursor ?? null, - has_more: data.has_more ?? false, - }; - } - if (Array.isArray(data.list)) { + + if (data && typeof data === 'object' && Array.isArray(data.list)) { + const isPageShape = + typeof data.page === 'number' && + typeof data.total_pages === 'number' && + data.next_cursor === undefined && + data.prev_cursor === undefined; + if (isPageShape) { const currentPage = data.page || 1; const totalPages = data.total_pages || 1; return { - items: data.list, + list: data.list, next_cursor: currentPage < totalPages ? String(currentPage + 1) : null, prev_cursor: currentPage > 1 ? String(currentPage - 1) : null, has_more: currentPage < totalPages, }; } + return { + list: data.list, + next_cursor: data.next_cursor ?? null, + prev_cursor: data.prev_cursor ?? null, + has_more: data.has_more ?? false, + }; } - + return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, @@ -399,7 +397,7 @@ class PostService { } catch (error) { console.error('搜索帖子失败:', error); return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, @@ -424,31 +422,33 @@ class PostService { ); const data = response.data; - - // 兼容两种 API 响应格式 - if (data && typeof data === 'object') { - if (Array.isArray(data.items)) { - return { - items: data.items, - next_cursor: data.next_cursor ?? null, - prev_cursor: data.prev_cursor ?? null, - has_more: data.has_more ?? false, - }; - } - if (Array.isArray(data.list)) { + + if (data && typeof data === 'object' && Array.isArray(data.list)) { + const isPageShape = + typeof data.page === 'number' && + typeof data.total_pages === 'number' && + data.next_cursor === undefined && + data.prev_cursor === undefined; + if (isPageShape) { const currentPage = data.page || 1; const totalPages = data.total_pages || 1; return { - items: data.list, + list: data.list, next_cursor: currentPage < totalPages ? String(currentPage + 1) : null, prev_cursor: currentPage > 1 ? String(currentPage - 1) : null, has_more: currentPage < totalPages, }; } + return { + list: data.list, + next_cursor: data.next_cursor ?? null, + prev_cursor: data.prev_cursor ?? null, + has_more: data.has_more ?? false, + }; } - + return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, @@ -456,7 +456,7 @@ class PostService { } catch (error) { console.error('获取用户帖子列表失败:', error); return { - items: [], + list: [], next_cursor: null, prev_cursor: null, has_more: false, diff --git a/src/types/dto.ts b/src/types/dto.ts index 111b7eb..b8cae24 100644 --- a/src/types/dto.ts +++ b/src/types/dto.ts @@ -489,7 +489,7 @@ export interface CursorPaginationRequest { */ export interface CursorPaginationResponse { /** 数据项列表 */ - items: T[]; + list: T[]; /** 下一页游标 */ next_cursor: string | null; /** 上一页游标 */