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:
@@ -2,6 +2,7 @@
|
||||
* GroupMembersScreen 群成员管理界面
|
||||
* 显示群成员列表,支持管理员管理成员
|
||||
* 支持响应式网格布局
|
||||
* 使用游标分页
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
@@ -27,6 +28,7 @@ import { groupService } from '../../services/groupService';
|
||||
import { groupManager } from '../../stores/groupManager';
|
||||
import { Avatar, Text, Button, Loading, EmptyState, Divider, ResponsiveContainer } from '../../components/common';
|
||||
import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import {
|
||||
GroupMemberResponse,
|
||||
GroupRole,
|
||||
@@ -68,12 +70,34 @@ const GroupMembersScreen: React.FC = () => {
|
||||
return GRID_CONFIG.mobile;
|
||||
}, [width]);
|
||||
|
||||
// 成员列表状态
|
||||
const [members, setMembers] = useState<GroupMemberResponse[]>([]);
|
||||
// 使用游标分页 Hook 管理成员列表
|
||||
const {
|
||||
items: members,
|
||||
isLoading,
|
||||
isRefreshing,
|
||||
hasMore,
|
||||
loadMore,
|
||||
refresh,
|
||||
error,
|
||||
} = useCursorPagination(
|
||||
async ({ cursor, pageSize }) => {
|
||||
return await groupService.getGroupMembersCursor(groupId, {
|
||||
cursor,
|
||||
page_size: pageSize,
|
||||
});
|
||||
},
|
||||
{ pageSize: 50 }
|
||||
);
|
||||
|
||||
// 本地成员状态(用于乐观更新)
|
||||
const [localMembers, setLocalMembers] = useState<GroupMemberResponse[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
|
||||
// 同步分页数据到本地状态
|
||||
useEffect(() => {
|
||||
setLocalMembers(members);
|
||||
setLoading(false);
|
||||
}, [members]);
|
||||
|
||||
// 当前用户的成员信息
|
||||
const [currentMember, setCurrentMember] = useState<GroupMemberResponse | null>(null);
|
||||
@@ -91,65 +115,31 @@ const GroupMembersScreen: React.FC = () => {
|
||||
const isOwner = currentMember?.role === 'owner';
|
||||
const isAdmin = currentMember?.role === 'admin' || isOwner;
|
||||
|
||||
// 加载成员列表
|
||||
const loadMembers = useCallback(
|
||||
async (
|
||||
pageNum: number = 1,
|
||||
refresh: boolean = false,
|
||||
forceRefresh: boolean = false
|
||||
) => {
|
||||
if (!hasMore && !refresh) return;
|
||||
|
||||
try {
|
||||
const response = await groupManager.getMembers(groupId, pageNum, 50, forceRefresh);
|
||||
|
||||
if (refresh) {
|
||||
setMembers(response.list);
|
||||
setPage(1);
|
||||
} else {
|
||||
setMembers(prev => [...prev, ...response.list]);
|
||||
}
|
||||
|
||||
setHasMore(response.list.length === 50);
|
||||
|
||||
// 查找当前用户的成员信息
|
||||
const myMember = response.list.find(m => m.user_id === currentUser?.id);
|
||||
if (myMember) {
|
||||
setCurrentMember(myMember);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载成员列表失败:', error);
|
||||
// 查找当前用户的成员信息
|
||||
useEffect(() => {
|
||||
const myMember = localMembers.find(m => m.user_id === currentUser?.id);
|
||||
if (myMember) {
|
||||
setCurrentMember(myMember);
|
||||
}
|
||||
}, [localMembers, currentUser]);
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
await refresh();
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}, [groupId, currentUser, hasMore]);
|
||||
}, [refresh]);
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
loadMembers(1, true, true);
|
||||
refresh();
|
||||
}, [groupId]);
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
setHasMore(true);
|
||||
loadMembers(1, true, true);
|
||||
}, [loadMembers]);
|
||||
|
||||
// 加载更多
|
||||
const loadMore = useCallback(() => {
|
||||
if (!loading && hasMore) {
|
||||
const nextPage = page + 1;
|
||||
setPage(nextPage);
|
||||
loadMembers(nextPage);
|
||||
}
|
||||
}, [loading, hasMore, page, loadMembers]);
|
||||
|
||||
// 按角色分组
|
||||
const groupMembers = useCallback((): MemberGroup[] => {
|
||||
const owners = members.filter(m => m.role === 'owner');
|
||||
const admins = members.filter(m => m.role === 'admin');
|
||||
const normalMembers = members.filter(m => m.role === 'member');
|
||||
const owners = localMembers.filter(m => m.role === 'owner');
|
||||
const admins = localMembers.filter(m => m.role === 'admin');
|
||||
const normalMembers = localMembers.filter(m => m.role === 'member');
|
||||
|
||||
const groups: MemberGroup[] = [];
|
||||
|
||||
@@ -164,7 +154,7 @@ const GroupMembersScreen: React.FC = () => {
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, [members]);
|
||||
}, [localMembers]);
|
||||
|
||||
// 打开操作菜单
|
||||
const openActionModal = (member: GroupMemberResponse) => {
|
||||
@@ -203,7 +193,7 @@ const GroupMembersScreen: React.FC = () => {
|
||||
});
|
||||
|
||||
// 更新本地数据
|
||||
setMembers(prev => prev.map(m => {
|
||||
setLocalMembers(prev => prev.map(m => {
|
||||
if (m.user_id === selectedMember.user_id) {
|
||||
return { ...m, role: newRole };
|
||||
}
|
||||
@@ -245,14 +235,14 @@ const GroupMembersScreen: React.FC = () => {
|
||||
await groupService.muteMember(groupId, selectedMember.user_id, newMuted ? -1 : 0);
|
||||
|
||||
// 更新本地数据
|
||||
setMembers(prev => prev.map(m => {
|
||||
setLocalMembers(prev => prev.map(m => {
|
||||
if (m.user_id === selectedMember.user_id) {
|
||||
return { ...m, muted: newMuted };
|
||||
}
|
||||
return m;
|
||||
}));
|
||||
// 强制刷新远端状态,避免命中旧缓存导致解禁后仍显示禁言
|
||||
await loadMembers(1, true, true);
|
||||
await refresh();
|
||||
|
||||
setActionModalVisible(false);
|
||||
Alert.alert('成功', `已${actionText}`);
|
||||
@@ -286,7 +276,7 @@ const GroupMembersScreen: React.FC = () => {
|
||||
await groupService.removeMember(groupId, selectedMember.user_id);
|
||||
|
||||
// 更新本地数据
|
||||
setMembers(prev => prev.filter(m => m.user_id !== selectedMember.user_id));
|
||||
setLocalMembers(prev => prev.filter(m => m.user_id !== selectedMember.user_id));
|
||||
|
||||
setActionModalVisible(false);
|
||||
Alert.alert('成功', '已移除成员');
|
||||
@@ -320,7 +310,7 @@ const GroupMembersScreen: React.FC = () => {
|
||||
});
|
||||
|
||||
// 更新本地数据
|
||||
setMembers(prev => prev.map(m => {
|
||||
setLocalMembers(prev => prev.map(m => {
|
||||
if (m.user_id === selectedMember.user_id) {
|
||||
return { ...m, nickname: newNickname.trim() };
|
||||
}
|
||||
@@ -422,7 +412,7 @@ const GroupMembersScreen: React.FC = () => {
|
||||
|
||||
// 渲染空状态
|
||||
const renderEmpty = () => {
|
||||
if (loading) return <Loading />;
|
||||
if (loading || isLoading) return <Loading />;
|
||||
|
||||
return (
|
||||
<EmptyState
|
||||
@@ -589,7 +579,7 @@ const GroupMembersScreen: React.FC = () => {
|
||||
keyExtractor={(item) => item.title}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={[colors.primary.main]}
|
||||
tintColor={colors.primary.main}
|
||||
@@ -598,6 +588,23 @@ const GroupMembersScreen: React.FC = () => {
|
||||
onEndReached={loadMore}
|
||||
onEndReachedThreshold={0.3}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListFooterComponent={
|
||||
isLoading ? (
|
||||
<View style={styles.loadingFooter}>
|
||||
<Loading size="sm" />
|
||||
</View>
|
||||
) : hasMore ? (
|
||||
<TouchableOpacity style={styles.loadMoreBtn} onPress={loadMore}>
|
||||
<Text variant="caption" color={colors.primary.main}>
|
||||
加载更多成员
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : localMembers.length > 0 ? (
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.noMoreText}>
|
||||
没有更多成员了
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
renderItem={({ item: group }) => (
|
||||
<View style={styles.section}>
|
||||
{renderSectionHeader(group.title, group.data.length)}
|
||||
@@ -724,6 +731,19 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
marginHorizontal: spacing.xs,
|
||||
},
|
||||
// 分页加载样式
|
||||
loadingFooter: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadMoreBtn: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
noMoreText: {
|
||||
textAlign: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
},
|
||||
});
|
||||
|
||||
export default GroupMembersScreen;
|
||||
|
||||
Reference in New Issue
Block a user