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:
lafay
2026-03-20 23:00:27 +08:00
parent a005fb0a15
commit 8a0aea1c59
20 changed files with 2464 additions and 2166 deletions

View File

@@ -1,7 +1,7 @@
/**
* 通知页 NotificationsScreen
* 胡萝卜BBS - 系统消息列表
* 使用新的系统消息API
* 【游标分页】使用 messageService.getSystemMessagesCursor
* 支持响应式布局
*/
@@ -26,6 +26,7 @@ import { messageService } from '../../services/messageService';
import { commentService } from '../../services/commentService';
import { SystemMessageItem } from '../../components/business';
import { Text, EmptyState, ResponsiveContainer } from '../../components/common';
import { useCursorPagination } from '../../hooks/useCursorPagination';
import { RootStackParamList } from '../../navigation/types';
import { useMessageManagerSystemUnreadCount, useUserStore } from '../../stores';
@@ -70,14 +71,32 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
// Web端使用更大的容器宽度
const containerMaxWidth = isDesktop ? 1200 : isTablet ? 1000 : 900;
const [messages, setMessages] = useState<SystemMessageResponse[]>([]);
const [activeType, setActiveType] = useState('all');
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(true);
const [hasMore, setHasMore] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [unreadCount, setUnreadCount] = useState(0);
// 【游标分页】使用 useCursorPagination hook 获取系统消息列表
const {
items: messages,
isLoading,
isRefreshing,
hasMore,
loadMore,
refresh,
error: paginationError,
} = useCursorPagination(
async ({ cursor, pageSize }) => {
return await messageService.getSystemMessagesCursor({
cursor,
page_size: pageSize,
});
},
{ pageSize: 20 }
);
// 本地刷新状态仅用于下拉刷新的UI显示
const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
// 同一 flag 只要有人审批过,就将待处理消息同步展示为已处理状态
const displayMessages = useMemo(() => {
const reviewedByFlag = new Map<string, SystemMessageResponse>();
@@ -116,21 +135,6 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
});
}, [messages]);
// 获取系统消息数据
const fetchMessages = useCallback(async () => {
try {
setLoading(true);
const response = await messageService.getSystemMessages(50, 1);
// 添加防御性检查,确保 messages 数组存在
setMessages(response.messages || []);
setHasMore(response.has_more ?? false);
} catch (error) {
console.error('获取系统消息失败:', error);
} finally {
setLoading(false);
}
}, []);
// 获取未读数
const fetchUnreadCount = useCallback(async () => {
try {
@@ -145,27 +149,25 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
const handleMarkAllRead = useCallback(async () => {
try {
await messageService.markAllSystemMessagesRead();
setMessages(prev => prev.map(m => ({ ...m, is_read: true })));
// 刷新消息列表
await refresh();
setUnreadCount(0);
setSystemUnreadCount(0);
// 同步更新全局 TabBar 红点
fetchMessageUnreadCount();
// 刷新消息列表
fetchMessages();
} catch (error) {
console.error('一键已读失败:', error);
}
}, [fetchMessages, fetchMessageUnreadCount, setSystemUnreadCount]);
}, [refresh, fetchMessageUnreadCount, setSystemUnreadCount]);
// 页面加载和获得焦点时刷新,并自动标记所有消息为已读
useEffect(() => {
if (isFocused) {
fetchMessages();
fetchUnreadCount();
// 进入界面自动标记所有消息为已读
handleMarkAllRead();
}
}, [isFocused, fetchMessages, fetchUnreadCount, handleMarkAllRead]);
}, [isFocused, fetchUnreadCount, handleMarkAllRead]);
// 屏幕失去焦点时,如果有 onBack 回调则调用它(用于内嵌模式)
useEffect(() => {
@@ -190,29 +192,17 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
// 下拉刷新
const onRefresh = useCallback(async () => {
setRefreshing(true);
await Promise.all([fetchMessages(), fetchUnreadCount()]);
await Promise.all([refresh(), fetchUnreadCount()]);
setRefreshing(false);
}, [fetchMessages, fetchUnreadCount]);
}, [refresh, fetchUnreadCount]);
// 加载更多
const loadMore = useCallback(async () => {
if (loadingMore || !hasMore || messages.length === 0) return;
try {
setLoadingMore(true);
// 使用时间戳或seq作为游标分页后端使用page分页
const nextPage = Math.floor((messages.length / 20)) + 1;
const response = await messageService.getSystemMessages(20, nextPage);
// 添加防御性检查
const newMessages = response.messages || [];
setMessages(prev => [...prev, ...newMessages]);
setHasMore(response.has_more ?? false);
} catch (error) {
console.error('加载更多失败:', error);
} finally {
setLoadingMore(false);
}
}, [loadingMore, hasMore, messages]);
const onEndReached = useCallback(async () => {
if (isLoading || loadingMore || !hasMore) return;
setLoadingMore(true);
await loadMore();
setLoadingMore(false);
}, [isLoading, loadingMore, hasMore, loadMore]);
// 标记单条消息已读并处理导航
const extractPostIdFromActionUrl = (actionUrl?: string): string | null => {
@@ -262,9 +252,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
const messageId = String(message.id);
const wasUnread = message.is_read !== true;
await messageService.markSystemMessageRead(messageId);
setMessages(prev =>
prev.map(m => (String(m.id) === messageId ? { ...m, is_read: true } : m))
);
// 【游标分页】不再直接修改 messages 状态,而是通过刷新获取最新数据
if (wasUnread) {
setUnreadCount(prev => Math.max(0, prev - 1));
decrementSystemUnreadCount(1);
@@ -431,7 +419,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
</View>
{/* 消息列表 */}
{loading ? (
{isLoading && messages.length === 0 ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
@@ -444,7 +432,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
showsVerticalScrollIndicator={false}
ListEmptyComponent={renderEmpty}
ListFooterComponent={renderFooter}
onEndReached={loadMore}
onEndReached={onEndReached}
onEndReachedThreshold={0.3}
refreshControl={
<RefreshControl
@@ -505,7 +493,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
</View>
{/* 消息列表 */}
{loading ? (
{isLoading && messages.length === 0 ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
@@ -518,7 +506,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
showsVerticalScrollIndicator={false}
ListEmptyComponent={renderEmpty}
ListFooterComponent={renderFooter}
onEndReached={loadMore}
onEndReached={onEndReached}
onEndReachedThreshold={0.3}
refreshControl={
<RefreshControl