feat(pagination): implement cursor-based pagination across the app
Add useCursorPagination hook and update multiple screens and services to use cursor-based pagination for better performance and consistency. - Add useCursorPagination hook with deduplication and caching support - Add cursor pagination types to infrastructure layer - Refactor HomeScreen, PostDetailScreen, SearchScreen for posts/comments - Refactor GroupMembersScreen, JoinGroupScreen for groups/members - Refactor MessageListScreen, NotificationsScreen for messages - Update post, message, group, comment, notification services with cursor endpoints - Add CursorPaginationRequest/Response DTOs - Remove deprecated OPTIMIZATION_DESIGN.md documentation
This commit is contained in:
@@ -4,13 +4,14 @@
|
||||
* 支持响应式布局,宽屏下显示更大的搜索结果区域
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
RefreshControl,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
@@ -21,13 +22,15 @@ import { Post, User } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
import { postService, authService } from '../../services';
|
||||
import { PostCard, TabBar, SearchBar } from '../../components/business';
|
||||
import { Avatar, EmptyState, Text, ResponsiveGrid } from '../../components/common';
|
||||
import { Avatar, EmptyState, Text, ResponsiveGrid, Loading } from '../../components/common';
|
||||
import { HomeStackParamList } from '../../navigation/types';
|
||||
import { useResponsive, useResponsiveSpacing, useResponsiveValue } from '../../hooks/useResponsive';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<HomeStackParamList, 'Search'>;
|
||||
|
||||
const TABS = ['帖子', '用户'];
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
type SearchType = 'posts' | 'users';
|
||||
|
||||
@@ -74,17 +77,47 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [searchResults, setSearchResults] = useState<{
|
||||
posts: Post[];
|
||||
users: User[];
|
||||
}>({
|
||||
posts: [],
|
||||
users: [],
|
||||
});
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
// 保存当前搜索关键词,用于Tab切换时重新搜索
|
||||
const [currentKeyword, setCurrentKeyword] = useState('');
|
||||
|
||||
// 使用游标分页进行帖子搜索
|
||||
const {
|
||||
items: searchResults,
|
||||
isLoading,
|
||||
isRefreshing,
|
||||
hasMore,
|
||||
loadMore,
|
||||
refresh,
|
||||
reset,
|
||||
} = useCursorPagination<Post, { query: string }>(
|
||||
async ({ cursor, pageSize, extraParams }) => {
|
||||
if (!extraParams?.query) {
|
||||
return { items: [], next_cursor: null, prev_cursor: null, has_more: false };
|
||||
}
|
||||
const response = await postService.searchPostsCursor(extraParams.query, {
|
||||
cursor,
|
||||
page_size: pageSize,
|
||||
});
|
||||
return response;
|
||||
},
|
||||
{ pageSize: DEFAULT_PAGE_SIZE },
|
||||
{ query: '' }
|
||||
);
|
||||
|
||||
// 用户搜索结果(保持原有分页方式)
|
||||
const [userResults, setUserResults] = useState<User[]>([]);
|
||||
const [userLoading, setUserLoading] = useState(false);
|
||||
|
||||
// 当搜索词变化时重置
|
||||
useEffect(() => {
|
||||
if (currentKeyword) {
|
||||
refresh();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
}, [currentKeyword]);
|
||||
|
||||
// 执行搜索 - 根据当前Tab执行对应类型的搜索
|
||||
const performSearch = useCallback(async (keyword: string) => {
|
||||
if (!keyword.trim()) return;
|
||||
@@ -101,26 +134,20 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
const searchType = getSearchType();
|
||||
|
||||
if (searchType === 'posts') {
|
||||
// 搜索帖子
|
||||
const postsResponse = await postService.searchPosts(trimmedKeyword, 1, 20);
|
||||
setSearchResults(prev => ({
|
||||
...prev,
|
||||
posts: postsResponse.list
|
||||
}));
|
||||
// 帖子搜索由 useCursorPagination 处理,这里只需触发刷新
|
||||
refresh();
|
||||
} else if (searchType === 'users') {
|
||||
// 搜索用户
|
||||
// 用户搜索保持原有方式
|
||||
setUserLoading(true);
|
||||
const usersResponse = await authService.searchUsers(trimmedKeyword, 1, 20);
|
||||
setSearchResults(prev => ({
|
||||
...prev,
|
||||
users: usersResponse.list
|
||||
}));
|
||||
setUserResults(usersResponse.list || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error);
|
||||
}
|
||||
|
||||
setHasSearched(true);
|
||||
}, [addSearchHistory, activeIndex]);
|
||||
}, [addSearchHistory, activeIndex, refresh]);
|
||||
|
||||
// 处理搜索提交
|
||||
const handleSearch = () => {
|
||||
@@ -159,9 +186,9 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
|
||||
// 渲染帖子搜索结果(使用响应式网格)
|
||||
const renderPostResults = () => {
|
||||
const posts = searchResults.posts;
|
||||
const posts = searchResults;
|
||||
|
||||
if (posts.length === 0) {
|
||||
if (posts.length === 0 && !isLoading) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="未找到相关帖子"
|
||||
@@ -177,6 +204,14 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingBottom: responsivePadding }}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={refresh}
|
||||
colors={[colors.primary.main]}
|
||||
tintColor={colors.primary.main}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ResponsiveGrid
|
||||
columns={{ xs: 1, sm: 2, md: 2, lg: 3, xl: 3, '2xl': 4, '3xl': 4, '4xl': 5 }}
|
||||
@@ -197,6 +232,11 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
/>
|
||||
))}
|
||||
</ResponsiveGrid>
|
||||
{isLoading && (
|
||||
<View style={styles.loadingMore}>
|
||||
<Loading size="sm" />
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -220,15 +260,26 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
keyExtractor={item => item.id}
|
||||
contentContainerStyle={{ paddingBottom: responsivePadding }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={refresh}
|
||||
colors={[colors.primary.main]}
|
||||
tintColor={colors.primary.main}
|
||||
/>
|
||||
}
|
||||
onEndReached={loadMore}
|
||||
onEndReachedThreshold={0.3}
|
||||
ListFooterComponent={isLoading ? <Loading size="sm" /> : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染用户搜索结果
|
||||
const renderUserResults = () => {
|
||||
const users = searchResults.users;
|
||||
const users = userResults;
|
||||
|
||||
if (users.length === 0) {
|
||||
if (users.length === 0 && !userLoading) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="未找到相关用户"
|
||||
@@ -290,6 +341,11 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ResponsiveGrid>
|
||||
{userLoading && (
|
||||
<View style={styles.loadingMore}>
|
||||
<Loading size="sm" />
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -329,6 +385,7 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
keyExtractor={item => item.id}
|
||||
contentContainerStyle={{ paddingVertical: responsiveGap }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListFooterComponent={userLoading ? <Loading size="sm" /> : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -501,20 +558,20 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
tabWrapper: {
|
||||
backgroundColor: colors.background.paper,
|
||||
paddingTop: spacing.xs,
|
||||
paddingBottom: spacing.xs,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: `${colors.divider}50`,
|
||||
},
|
||||
suggestionsContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
section: {
|
||||
marginTop: spacing.lg,
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: spacing.md,
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontWeight: '600',
|
||||
@@ -528,17 +585,32 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider,
|
||||
},
|
||||
tagText: {
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
// 移动端用户列表样式
|
||||
userCard: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
userCardInfo: {
|
||||
flex: 1,
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
userCardName: {
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
},
|
||||
userItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
},
|
||||
userInfo: {
|
||||
flex: 1,
|
||||
@@ -549,28 +621,15 @@ const styles = StyleSheet.create({
|
||||
color: colors.text.primary,
|
||||
},
|
||||
followingBadge: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: colors.primary.light + '30',
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: `${colors.primary.main}14`,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
// 桌面端用户卡片样式
|
||||
userCard: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
loadingMore: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: 180,
|
||||
},
|
||||
userCardInfo: {
|
||||
alignItems: 'center',
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
userCardName: {
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user