refactor: standardize pagination response structure and enhance cursor handling
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 5m8s
Frontend CI / ota-android (push) Successful in 11m22s
Frontend CI / build-android-apk (push) Successful in 45m46s

- Update various services and hooks to replace 'items' with 'list' for consistency in pagination responses.
- Modify PostRepository, commentService, groupService, messageService, and postService to align with the new response structure.
- Enhance cursor pagination logic in hooks and components to ensure proper handling of pagination states.
- Refactor HomeScreen, PostDetailScreen, SearchScreen, and other components to utilize the updated pagination structure.
This commit is contained in:
lafay
2026-03-23 03:58:26 +08:00
parent 88510aa6ae
commit 26ae288470
19 changed files with 312 additions and 309 deletions

View File

@@ -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: |

View File

@@ -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<PostsResult> {
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<PostsResult> {
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,

View File

@@ -231,9 +231,12 @@ export class PostRepository implements IPostRepository {
try {
const queryParams: Record<string, any> = {};
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,
};

View File

@@ -29,7 +29,7 @@ export function useCursorPagination<T, P = void>(
const effectivePageSize = Math.min(Math.max(1, pageSize), MAX_PAGE_SIZE);
const [state, setState] = useState<CursorPaginationState<T>>({
items: [],
list: [],
nextCursor: null,
prevCursor: null,
hasMore: true, // 初始设为 true以便首次加载可以执行
@@ -86,7 +86,7 @@ export function useCursorPagination<T, P = void>(
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<T, P = void>(
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<T, P = void>(
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<T, P = void>(
isFirstLoadRef.current = true;
isLoadingRef.current = false;
setState({
items: [],
list: [],
nextCursor: null,
prevCursor: null,
hasMore: true, // 重置后也设为 true以便可以重新加载
@@ -208,16 +208,16 @@ export function useCursorPagination<T, P = void>(
}, []);
// 设置数据(用于外部数据注入)
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<T, P = void>(
// 自动加载第一页
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<T, P = void>(
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<T, P = void>(
loadPrevious,
refresh,
reset,
setItems,
setList,
};
}

View File

@@ -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<T extends PostIdentifier = Post>(
enableDiff = true,
enableBatching = true,
maxPostCount = 10000,
changeRatioThreshold = 0.5,
listKey = 'default',
autoSubscribe = true,
} = options;
@@ -147,37 +145,7 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
const previousPostsRef = useRef<T[]>(initialPosts);
const unsubscribeRef = useRef<(() => void) | null>(null);
// ==================== 初始化 ====================
// 初始化差异计算器
useEffect(() => {
if (enableDiff) {
calculatorRef.current = createPostDiffCalculator<T>(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<T extends PostIdentifier = Post>(
}
}, []);
// ==================== 差异计算 ====================
// ==================== 初始化 ====================
/**
* 计算并应用差异
*/
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<T>(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<T extends PostIdentifier = Post>(
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<T extends PostIdentifier = Post>(
}
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(() => {

View File

@@ -205,7 +205,7 @@ export interface CursorPaginationConfig {
*/
export interface CursorPaginationState<T> {
/** 数据项列表 */
items: T[];
list: T[];
/** 下一页游标 */
nextCursor: string | null;
/** 上一页游标 */
@@ -235,7 +235,7 @@ export interface CursorPaginationActions<T> {
/** 重置状态 */
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<T, P = void> = (params: {
/** 额外参数 */
extraParams?: P;
}) => Promise<{
items: T[];
list: T[];
next_cursor: string | null;
prev_cursor: string | null;
has_more: boolean;

View File

@@ -89,6 +89,10 @@ export const HomeScreen: React.FC = () => {
const postIdsRef = useRef<Set<string>>(new Set());
const lastSwipeAtRef = useRef(0);
// 网格模式滚动位置检测
const gridScrollYRef = useRef(0);
const isLoadingMoreRef = useRef(false);
// 构建一个以 postId 为 key 的 map用于快速查找
const postsMap = useMemo(() => {
const map = new Map<string, Post>();
@@ -131,15 +135,31 @@ 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 () => {
try {
@@ -485,6 +505,7 @@ export const HomeScreen: React.FC = () => {
]}
showsVerticalScrollIndicator={false}
scrollEventThrottle={100}
onScroll={handleGridScroll}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
@@ -495,6 +516,12 @@ export const HomeScreen: React.FC = () => {
}
>
{distributePostsToColumns.map((column, index) => renderWaterfallColumn(column, index))}
{/* 加载更多指示器 */}
{isLoading && displayPosts.length > 0 && (
<View style={styles.loadingMore}>
<Loading size="sm" />
</View>
)}
</ScrollView>
);
}
@@ -767,4 +794,9 @@ const styles = StyleSheet.create({
height: 72,
borderRadius: 36,
},
loadingMore: {
paddingVertical: 20,
alignItems: 'center',
justifyContent: 'center',
},
});

View File

@@ -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 = () => {
</Text>
</TouchableOpacity>
) : comments.length > 0 ? (
) : (comments?.length ?? 0) > 0 ? (
<Text variant="caption" color={colors.text.hint} style={styles.noMoreComments}>
</Text>
@@ -1546,7 +1546,7 @@ export const PostDetailScreen: React.FC = () => {
</Text>
</TouchableOpacity>
) : comments.length > 0 ? (
) : (comments?.length ?? 0) > 0 ? (
<Text variant="caption" color={colors.text.hint} style={styles.noMoreComments}>
</Text>

View File

@@ -83,7 +83,7 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
// 使用游标分页进行帖子搜索
const {
items: searchResults,
list: searchResults,
isLoading,
isRefreshing,
hasMore,
@@ -93,7 +93,7 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
} = useCursorPagination<Post, { query: string }>(
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,

View File

@@ -72,7 +72,7 @@ const GroupMembersScreen: React.FC = () => {
// 使用游标分页 Hook 管理成员列表
const {
items: members,
list: members,
isLoading,
isRefreshing,
hasMore,

View File

@@ -36,7 +36,7 @@ const JoinGroupScreen: React.FC = () => {
// 使用游标分页 Hook 管理群组列表
const {
items: groups,
list: groups,
isLoading,
isRefreshing,
hasMore,

View File

@@ -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 = () => {
</View>
</TouchableOpacity>
{isLoading && conversations.length === 0 ? (
{isLoading && conversationList.length === 0 ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>

View File

@@ -88,7 +88,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
);
const {
items: messages,
list: messages,
isLoading,
isRefreshing,
hasMore,

View File

@@ -255,15 +255,18 @@ class CommentService {
params: CursorPaginationRequest = {}
): Promise<CursorPaginationResponse<Comment>> {
try {
const response = await api.get<CursorPaginationResponse<Comment>>(
`/comments/post/${postId}/cursor`,
{ params }
);
return response.data;
const response = await api.get<any>(`/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<CursorPaginationResponse<Comment>> {
try {
const response = await api.get<CursorPaginationResponse<Comment>>(
`/comments/${commentId}/replies/cursor`,
{ params }
);
return response.data;
const response = await api.get<any>(`/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,

View File

@@ -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,

View File

@@ -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<CursorPaginationResponse<MessageResponse>> {
try {
const response = await api.get<CursorPaginationResponse<MessageResponse>>(
const response = await api.get<any>(
`/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 的分页(统一为 CursorPaginationResponselist
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,

View File

@@ -172,7 +172,7 @@ class NotificationService {
} catch (error) {
console.error('获取通知列表失败:', error);
return {
items: [],
list: [],
next_cursor: null,
prev_cursor: null,
has_more: false,

View File

@@ -305,36 +305,32 @@ class PostService {
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,
@@ -368,30 +364,32 @@ 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,
@@ -425,30 +423,32 @@ 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,

View File

@@ -489,7 +489,7 @@ export interface CursorPaginationRequest {
*/
export interface CursorPaginationResponse<T> {
/** 数据项列表 */
items: T[];
list: T[];
/** 下一页游标 */
next_cursor: string | null;
/** 上一页游标 */