/** * GroupMembersScreen 群成员管理界面 * 显示群成员列表,支持管理员管理成员 * 支持响应式网格布局 * 使用游标分页 */ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { View, StyleSheet, FlatList, TouchableOpacity, Alert, RefreshControl, ListRenderItem, Modal, TextInput, Dimensions, Platform, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { hrefUserProfile } from '../../navigation/hrefs'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { blurActiveElement } from '../../infrastructure/platform'; import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; import { useAuthStore } from '../../stores'; import { groupService } from '@/services/message'; import { createCursorGroupMemberListSource, GroupMemberListSourceKind, ICursorGroupMemberListPagedSource, } from '../../stores/group'; import { enrichGroupMembersWithUserProfiles } from '../../stores/group'; import { Avatar, Text, Button, Loading, EmptyState, Divider, ResponsiveContainer, AppBackButton } from '../../components/common'; import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive'; import { useCursorPagination } from '../../hooks/useCursorPagination'; import { GroupMemberResponse, GroupRole, } from '../../types/dto'; import { firstRouteParam } from '../../navigation/paramUtils'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); const GROUP_MEMBER_REMOTE_LIST_KIND: GroupMemberListSourceKind = 'cursor'; // 网格布局配置 const GRID_CONFIG = { mobile: { columns: 1, itemWidth: '100%' }, tablet: { columns: 2, itemWidth: '48%' }, desktop: { columns: 3, itemWidth: '31%' }, }; // 成员分组 interface MemberGroup { title: string; data: GroupMemberResponse[]; } const GroupMembersScreen: React.FC = () => { const colors = useAppColors(); const styles = useMemo(() => createGroupMembersStyles(colors), [colors]); const router = useRouter(); const { groupId: groupIdParam } = useLocalSearchParams<{ groupId?: string | string[] }>(); const groupId = firstRouteParam(groupIdParam) ?? ''; const { currentUser } = useAuthStore(); // 响应式布局 const { isDesktop, isTablet, width } = useResponsive(); const isWideScreen = useBreakpointGTE('lg'); // 计算网格列数 const gridConfig = useMemo(() => { if (width >= 1024) return GRID_CONFIG.desktop; if (width >= 768) return GRID_CONFIG.tablet; return GRID_CONFIG.mobile; }, [width]); const memberListSource = useMemo(() => { return createCursorGroupMemberListSource(GROUP_MEMBER_REMOTE_LIST_KIND, groupId, 50); }, [groupId]); // 使用统一数据源 + 游标 Hook 管理成员列表 const { list: members, isLoading, isRefreshing, hasMore, loadMore, refresh, error, } = useCursorPagination( async ({ cursor }) => { // cursor 为空表示第一页/刷新,此时重置数据源状态 if (!cursor) { memberListSource.restart(); } return await memberListSource.loadNext(); }, { pageSize: 50 } ); // 本地成员状态(用于乐观更新) const [localMembers, setLocalMembers] = useState([]); const [loading, setLoading] = useState(true); // 同步分页数据到本地状态 useEffect(() => { let active = true; const syncMembers = async () => { const enrichedMembers = await enrichGroupMembersWithUserProfiles(members); if (!active) return; setLocalMembers(enrichedMembers); setLoading(false); }; syncMembers(); return () => { active = false; }; }, [members]); // 当前用户的成员信息 const [currentMember, setCurrentMember] = useState(null); // 操作模态框状态 const [actionModalVisible, setActionModalVisible] = useState(false); const [selectedMember, setSelectedMember] = useState(null); const [actionLoading, setActionLoading] = useState(false); // 设置群昵称模态框 const [nicknameModalVisible, setNicknameModalVisible] = useState(false); const [newNickname, setNewNickname] = useState(''); useEffect(() => { if (actionModalVisible || nicknameModalVisible) { blurActiveElement(); } }, [actionModalVisible, nicknameModalVisible]); // 计算当前用户的角色 const isOwner = currentMember?.role === 'owner'; const isAdmin = currentMember?.role === 'admin' || isOwner; // 查找当前用户的成员信息 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); }, [refresh]); // 初始加载 - 使用 ref 避免依赖变化导致的无限循环 const initialLoadRef = React.useRef(false); useEffect(() => { if (!groupId || initialLoadRef.current) return; initialLoadRef.current = true; refresh(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [groupId]); // 按角色分组 const groupMembers = useCallback((): MemberGroup[] => { 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[] = []; if (owners.length > 0) { groups.push({ title: '群主', data: owners }); } if (admins.length > 0) { groups.push({ title: '管理员', data: admins }); } if (normalMembers.length > 0) { groups.push({ title: '成员', data: normalMembers }); } return groups; }, [localMembers]); // 打开操作菜单 const openActionModal = (member: GroupMemberResponse) => { // 不能操作群主 if (member.role === 'owner') return; // 普通成员不能操作其他人 if (!isAdmin) return; // 管理员不能操作其他管理员(只有群主可以) if (!isOwner && member.role === 'admin') return; setSelectedMember(member); setActionModalVisible(true); }; // 设置/取消管理员 const handleToggleAdmin = async () => { if (!selectedMember) return; const newRole: GroupRole = selectedMember.role === 'admin' ? 'member' : 'admin'; const actionText = newRole === 'admin' ? '设为管理员' : '取消管理员'; Alert.alert( actionText, `确定要${actionText} "${selectedMember.user?.nickname || selectedMember.nickname}" 吗?`, [ { text: '取消', style: 'cancel' }, { text: '确定', onPress: async () => { setActionLoading(true); try { await groupService.setMemberRole(groupId, selectedMember.user_id, { role: newRole as 'admin' | 'member', }); // 更新本地数据 setLocalMembers(prev => prev.map(m => { if (m.user_id === selectedMember.user_id) { return { ...m, role: newRole }; } return m; })); setActionModalVisible(false); Alert.alert('成功', `已${actionText}`); } catch (error: any) { console.error('设置角色失败:', error); Alert.alert('错误', error.message || '操作失败'); } finally { setActionLoading(false); } }, }, ] ); }; // 禁言/解禁成员 const handleToggleMute = async () => { if (!selectedMember) return; const newMuted = !selectedMember.muted; const actionText = newMuted ? '禁言' : '解禁'; Alert.alert( `${actionText}成员`, `确定要${actionText} "${selectedMember.user?.nickname || selectedMember.nickname}" 吗?`, [ { text: '取消', style: 'cancel' }, { text: '确定', onPress: async () => { setActionLoading(true); try { // duration: 0 表示解除禁言,-1 表示永久禁言 await groupService.muteMember(groupId, selectedMember.user_id, newMuted ? -1 : 0); // 更新本地数据 setLocalMembers(prev => prev.map(m => { if (m.user_id === selectedMember.user_id) { return { ...m, muted: newMuted }; } return m; })); // 强制刷新远端状态,避免命中旧缓存导致解禁后仍显示禁言 await refresh(); setActionModalVisible(false); Alert.alert('成功', `已${actionText}`); } catch (error: any) { console.error('禁言操作失败:', error); Alert.alert('错误', error.message || '操作失败'); } finally { setActionLoading(false); } }, }, ] ); }; // 移除成员 const handleRemoveMember = () => { if (!selectedMember) return; Alert.alert( '移除成员', `确定要将 "${selectedMember.user?.nickname || selectedMember.nickname}" 移出群聊吗?`, [ { text: '取消', style: 'cancel' }, { text: '移除', style: 'destructive', onPress: async () => { setActionLoading(true); try { await groupService.removeMember(groupId, selectedMember.user_id); // 更新本地数据 setLocalMembers(prev => prev.filter(m => m.user_id !== selectedMember.user_id)); setActionModalVisible(false); Alert.alert('成功', '已移除成员'); } catch (error: any) { console.error('移除成员失败:', error); Alert.alert('错误', error.message || '操作失败'); } finally { setActionLoading(false); } }, }, ] ); }; // 打开设置群昵称模态框 const openNicknameModal = (member: GroupMemberResponse) => { setSelectedMember(member); setNewNickname(member.nickname || member.user?.nickname || ''); setNicknameModalVisible(true); }; // 设置群昵称(仅限自己) const handleSetNickname = async () => { if (!selectedMember || selectedMember.user_id !== currentUser?.id) return; setActionLoading(true); try { await groupService.setNickname(groupId, { nickname: newNickname.trim() || selectedMember.user?.nickname || '', }); // 更新本地数据 setLocalMembers(prev => prev.map(m => { if (m.user_id === selectedMember.user_id) { return { ...m, nickname: newNickname.trim() }; } return m; })); setNicknameModalVisible(false); Alert.alert('成功', '群昵称已更新'); } catch (error: any) { console.error('设置群昵称失败:', error); Alert.alert('错误', error.message || '操作失败'); } finally { setActionLoading(false); } }; // 获取角色标签颜色 const getRoleBadgeColor = (role: GroupRole): string => { switch (role) { case 'owner': return colors.warning.main; case 'admin': return colors.primary.main; default: return colors.text.hint; } }; // 获取角色标签文本 const getRoleBadgeText = (role: GroupRole): string => { switch (role) { case 'owner': return '群主'; case 'admin': return '管理员'; default: return ''; } }; // 点击头像进入对方主页 const handleAvatarPress = (member: GroupMemberResponse) => { if (member.user_id) { router.push(hrefUserProfile(String(member.user_id))); } }; // 渲染成员项 const renderMember: ListRenderItem = ({ item }) => { const isSelf = item.user_id === currentUser?.id; const canManage = isAdmin && item.role !== 'owner' && (isOwner || item.role === 'member'); return ( handleAvatarPress(item)} activeOpacity={0.7} > isSelf ? openNicknameModal(item) : handleAvatarPress(item)} onLongPress={() => isSelf ? openNicknameModal(item) : null} activeOpacity={0.7} > {item.nickname || item.user?.nickname || '未知用户'} {item.role !== 'member' && ( {getRoleBadgeText(item.role)} )} @{item.user?.username || 'unknown'} {item.muted && ( 禁言中 )} {canManage && ( openActionModal(item)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} > )} ); }; // 渲染分组头部 const renderSectionHeader = (title: string, count: number) => ( {title} {count}人 ); // 渲染空状态 const renderEmpty = () => { if (loading || isLoading) return ; return ( ); }; // 渲染操作菜单 const renderActionModal = () => { if (!selectedMember) return null; return ( setActionModalVisible(false)} > {selectedMember.user?.nickname || selectedMember.nickname} @{selectedMember.user?.username} {/* 设置/取消管理员(仅群主) */} {isOwner && selectedMember.role !== 'owner' && ( {selectedMember.role === 'admin' ? '取消管理员' : '设为管理员'} )} {/* 禁言/解禁 */} {selectedMember.muted ? '解除禁言' : '禁言'} {/* 移除成员 */} 移除成员