2026-03-09 21:29:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* GroupMembersScreen 群成员管理界面
|
|
|
|
|
|
* 显示群成员列表,支持管理员管理成员
|
|
|
|
|
|
* 支持响应式网格布局
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
|
|
|
|
import {
|
|
|
|
|
|
View,
|
|
|
|
|
|
StyleSheet,
|
|
|
|
|
|
FlatList,
|
|
|
|
|
|
TouchableOpacity,
|
|
|
|
|
|
Alert,
|
|
|
|
|
|
RefreshControl,
|
|
|
|
|
|
ListRenderItem,
|
|
|
|
|
|
Modal,
|
|
|
|
|
|
TextInput,
|
|
|
|
|
|
Dimensions,
|
|
|
|
|
|
} from 'react-native';
|
|
|
|
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
|
|
|
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
|
|
|
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
|
|
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
|
|
|
|
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
|
|
|
|
|
|
import { useAuthStore } from '../../stores';
|
|
|
|
|
|
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 {
|
|
|
|
|
|
GroupMemberResponse,
|
|
|
|
|
|
GroupRole,
|
|
|
|
|
|
} from '../../types/dto';
|
|
|
|
|
|
import { RootStackParamList } from '../../navigation/types';
|
|
|
|
|
|
|
|
|
|
|
|
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
|
|
|
|
|
|
|
|
|
|
|
// 网格布局配置
|
|
|
|
|
|
const GRID_CONFIG = {
|
|
|
|
|
|
mobile: { columns: 1, itemWidth: '100%' },
|
|
|
|
|
|
tablet: { columns: 2, itemWidth: '48%' },
|
|
|
|
|
|
desktop: { columns: 3, itemWidth: '31%' },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
|
|
|
|
|
type GroupMembersRouteProp = RouteProp<RootStackParamList, 'GroupMembers'>;
|
|
|
|
|
|
|
|
|
|
|
|
// 成员分组
|
|
|
|
|
|
interface MemberGroup {
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
data: GroupMemberResponse[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const GroupMembersScreen: React.FC = () => {
|
|
|
|
|
|
const navigation = useNavigation<NavigationProp>();
|
|
|
|
|
|
const route = useRoute<GroupMembersRouteProp>();
|
|
|
|
|
|
const { groupId } = route.params;
|
|
|
|
|
|
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 [members, setMembers] = useState<GroupMemberResponse[]>([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
|
|
const [page, setPage] = useState(1);
|
|
|
|
|
|
const [hasMore, setHasMore] = useState(true);
|
|
|
|
|
|
|
|
|
|
|
|
// 当前用户的成员信息
|
|
|
|
|
|
const [currentMember, setCurrentMember] = useState<GroupMemberResponse | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 操作模态框状态
|
|
|
|
|
|
const [actionModalVisible, setActionModalVisible] = useState(false);
|
|
|
|
|
|
const [selectedMember, setSelectedMember] = useState<GroupMemberResponse | null>(null);
|
|
|
|
|
|
const [actionLoading, setActionLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置群昵称模态框
|
|
|
|
|
|
const [nicknameModalVisible, setNicknameModalVisible] = useState(false);
|
|
|
|
|
|
const [newNickname, setNewNickname] = useState('');
|
|
|
|
|
|
|
|
|
|
|
|
// 计算当前用户的角色
|
|
|
|
|
|
const isOwner = currentMember?.role === 'owner';
|
|
|
|
|
|
const isAdmin = currentMember?.role === 'admin' || isOwner;
|
|
|
|
|
|
|
|
|
|
|
|
// 加载成员列表
|
2026-03-10 12:58:23 +08:00
|
|
|
|
const loadMembers = useCallback(
|
|
|
|
|
|
async (
|
|
|
|
|
|
pageNum: number = 1,
|
|
|
|
|
|
refresh: boolean = false,
|
|
|
|
|
|
forceRefresh: boolean = false
|
|
|
|
|
|
) => {
|
2026-03-09 21:29:03 +08:00
|
|
|
|
if (!hasMore && !refresh) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
const response = await groupManager.getMembers(groupId, pageNum, 50, forceRefresh);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
setRefreshing(false);
|
|
|
|
|
|
}, [groupId, currentUser, hasMore]);
|
|
|
|
|
|
|
|
|
|
|
|
// 初始加载
|
|
|
|
|
|
useEffect(() => {
|
2026-03-10 12:58:23 +08:00
|
|
|
|
loadMembers(1, true, true);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}, [groupId]);
|
|
|
|
|
|
|
|
|
|
|
|
// 下拉刷新
|
|
|
|
|
|
const onRefresh = useCallback(() => {
|
|
|
|
|
|
setRefreshing(true);
|
|
|
|
|
|
setHasMore(true);
|
2026-03-10 12:58:23 +08:00
|
|
|
|
loadMembers(1, true, true);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}, [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 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;
|
|
|
|
|
|
}, [members]);
|
|
|
|
|
|
|
|
|
|
|
|
// 打开操作菜单
|
|
|
|
|
|
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',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 更新本地数据
|
|
|
|
|
|
setMembers(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);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新本地数据
|
|
|
|
|
|
setMembers(prev => prev.map(m => {
|
|
|
|
|
|
if (m.user_id === selectedMember.user_id) {
|
|
|
|
|
|
return { ...m, muted: newMuted };
|
|
|
|
|
|
}
|
|
|
|
|
|
return m;
|
|
|
|
|
|
}));
|
2026-03-10 12:58:23 +08:00
|
|
|
|
// 强制刷新远端状态,避免命中旧缓存导致解禁后仍显示禁言
|
|
|
|
|
|
await loadMembers(1, true, true);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新本地数据
|
|
|
|
|
|
setMembers(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 || '',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 更新本地数据
|
|
|
|
|
|
setMembers(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 renderMember: ListRenderItem<GroupMemberResponse> = ({ item }) => {
|
|
|
|
|
|
const isSelf = item.user_id === currentUser?.id;
|
|
|
|
|
|
const canManage = isAdmin && item.role !== 'owner' && (isOwner || item.role === 'member');
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={styles.memberItem}
|
|
|
|
|
|
onPress={() => canManage ? openActionModal(item) : null}
|
|
|
|
|
|
onLongPress={() => isSelf ? openNicknameModal(item) : null}
|
|
|
|
|
|
activeOpacity={canManage ? 0.7 : 1}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Avatar
|
|
|
|
|
|
source={item.user?.avatar}
|
|
|
|
|
|
size={48}
|
|
|
|
|
|
name={item.user?.nickname || item.nickname}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<View style={styles.memberInfo}>
|
|
|
|
|
|
<View style={styles.memberNameRow}>
|
|
|
|
|
|
<Text variant="body" style={styles.memberName}>
|
|
|
|
|
|
{item.nickname || item.user?.nickname || '未知用户'}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
{item.role !== 'member' && (
|
|
|
|
|
|
<View style={[styles.roleBadge, { backgroundColor: getRoleBadgeColor(item.role) }]}>
|
|
|
|
|
|
<Text variant="label" color={colors.background.paper}>
|
|
|
|
|
|
{getRoleBadgeText(item.role)}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary}>
|
|
|
|
|
|
@{item.user?.username || 'unknown'}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
{item.muted && (
|
|
|
|
|
|
<View style={styles.mutedBadge}>
|
|
|
|
|
|
<MaterialCommunityIcons name="microphone-off" size={12} color={colors.error.main} />
|
|
|
|
|
|
<Text variant="label" color={colors.error.main}> 禁言中</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
{canManage && (
|
|
|
|
|
|
<MaterialCommunityIcons name="dots-vertical" size={20} color={colors.text.hint} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染分组头部
|
|
|
|
|
|
const renderSectionHeader = (title: string, count: number) => (
|
|
|
|
|
|
<View style={styles.sectionHeader}>
|
|
|
|
|
|
<Text variant="label" color={colors.text.secondary}>
|
|
|
|
|
|
{title}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Text variant="caption" color={colors.text.hint}>
|
|
|
|
|
|
{count}人
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染空状态
|
|
|
|
|
|
const renderEmpty = () => {
|
|
|
|
|
|
if (loading) return <Loading />;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="暂无成员"
|
|
|
|
|
|
description="群组还没有成员"
|
|
|
|
|
|
icon="account-group-outline"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染操作菜单
|
|
|
|
|
|
const renderActionModal = () => {
|
|
|
|
|
|
if (!selectedMember) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
visible={actionModalVisible}
|
|
|
|
|
|
animationType="slide"
|
|
|
|
|
|
transparent
|
|
|
|
|
|
onRequestClose={() => setActionModalVisible(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<View style={styles.modalOverlay}>
|
|
|
|
|
|
<View style={styles.modalContent}>
|
|
|
|
|
|
<View style={styles.modalHeader}>
|
|
|
|
|
|
<Avatar
|
|
|
|
|
|
source={selectedMember.user?.avatar}
|
|
|
|
|
|
size={60}
|
|
|
|
|
|
name={selectedMember.user?.nickname || selectedMember.nickname}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Text variant="h3" style={styles.modalTitle}>
|
|
|
|
|
|
{selectedMember.user?.nickname || selectedMember.nickname}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary}>
|
|
|
|
|
|
@{selectedMember.user?.username}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
<Divider />
|
|
|
|
|
|
|
|
|
|
|
|
{/* 设置/取消管理员(仅群主) */}
|
|
|
|
|
|
{isOwner && selectedMember.role !== 'owner' && (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={styles.actionItem}
|
|
|
|
|
|
onPress={handleToggleAdmin}
|
|
|
|
|
|
disabled={actionLoading}
|
|
|
|
|
|
>
|
|
|
|
|
|
<MaterialCommunityIcons
|
|
|
|
|
|
name={selectedMember.role === 'admin' ? 'account-remove' : 'account-plus'}
|
|
|
|
|
|
size={22}
|
|
|
|
|
|
color={colors.text.primary}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Text variant="body" style={styles.actionText}>
|
|
|
|
|
|
{selectedMember.role === 'admin' ? '取消管理员' : '设为管理员'}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 禁言/解禁 */}
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={styles.actionItem}
|
|
|
|
|
|
onPress={handleToggleMute}
|
|
|
|
|
|
disabled={actionLoading}
|
|
|
|
|
|
>
|
|
|
|
|
|
<MaterialCommunityIcons
|
|
|
|
|
|
name={selectedMember.muted ? 'microphone' : 'microphone-off'}
|
|
|
|
|
|
size={22}
|
|
|
|
|
|
color={selectedMember.muted ? colors.success.main : colors.error.main}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Text
|
|
|
|
|
|
variant="body"
|
|
|
|
|
|
color={selectedMember.muted ? colors.success.main : colors.error.main}
|
|
|
|
|
|
style={styles.actionText}
|
|
|
|
|
|
>
|
|
|
|
|
|
{selectedMember.muted ? '解除禁言' : '禁言'}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 移除成员 */}
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={styles.actionItem}
|
|
|
|
|
|
onPress={handleRemoveMember}
|
|
|
|
|
|
disabled={actionLoading}
|
|
|
|
|
|
>
|
|
|
|
|
|
<MaterialCommunityIcons name="account-remove" size={22} color={colors.error.main} />
|
|
|
|
|
|
<Text variant="body" color={colors.error.main} style={styles.actionText}>
|
|
|
|
|
|
移除成员
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
|
|
|
|
|
|
<Divider />
|
|
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
|
title="取消"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onPress={() => setActionModalVisible(false)}
|
|
|
|
|
|
fullWidth
|
|
|
|
|
|
disabled={actionLoading}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染设置群昵称模态框
|
|
|
|
|
|
const renderNicknameModal = () => {
|
|
|
|
|
|
if (!selectedMember) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
visible={nicknameModalVisible}
|
|
|
|
|
|
animationType="slide"
|
|
|
|
|
|
transparent
|
|
|
|
|
|
onRequestClose={() => setNicknameModalVisible(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<View style={styles.modalOverlay}>
|
|
|
|
|
|
<View style={styles.modalContent}>
|
|
|
|
|
|
<Text variant="h3" style={styles.modalTitle}>设置群昵称</Text>
|
|
|
|
|
|
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary} style={styles.inputLabel}>
|
|
|
|
|
|
在本群的昵称(选填)
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<TextInput
|
|
|
|
|
|
style={styles.input}
|
|
|
|
|
|
value={newNickname}
|
|
|
|
|
|
onChangeText={setNewNickname}
|
|
|
|
|
|
placeholder={selectedMember.user?.nickname || '请输入群昵称'}
|
|
|
|
|
|
placeholderTextColor={colors.text.hint}
|
|
|
|
|
|
maxLength={20}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<View style={styles.modalButtons}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
title="取消"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onPress={() => setNicknameModalVisible(false)}
|
|
|
|
|
|
style={styles.modalButton}
|
|
|
|
|
|
disabled={actionLoading}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
title="保存"
|
|
|
|
|
|
onPress={handleSetNickname}
|
|
|
|
|
|
loading={actionLoading}
|
|
|
|
|
|
style={styles.modalButton}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染分组列表
|
|
|
|
|
|
const renderGroupedList = () => {
|
|
|
|
|
|
const groups = groupMembers();
|
|
|
|
|
|
|
|
|
|
|
|
if (groups.length === 0) {
|
|
|
|
|
|
return renderEmpty();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<FlatList
|
|
|
|
|
|
data={groups}
|
|
|
|
|
|
keyExtractor={(item) => item.title}
|
|
|
|
|
|
refreshControl={
|
|
|
|
|
|
<RefreshControl
|
|
|
|
|
|
refreshing={refreshing}
|
|
|
|
|
|
onRefresh={onRefresh}
|
|
|
|
|
|
colors={[colors.primary.main]}
|
|
|
|
|
|
tintColor={colors.primary.main}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
onEndReached={loadMore}
|
|
|
|
|
|
onEndReachedThreshold={0.3}
|
|
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
|
|
renderItem={({ item: group }) => (
|
|
|
|
|
|
<View style={styles.section}>
|
|
|
|
|
|
{renderSectionHeader(group.title, group.data.length)}
|
|
|
|
|
|
{group.data.map((member, index) => (
|
|
|
|
|
|
<View key={member.id}>
|
|
|
|
|
|
{renderMember({ item: member, index } as any)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
|
|
|
|
|
{loading ? <Loading /> : renderGroupedList()}
|
|
|
|
|
|
{renderActionModal()}
|
|
|
|
|
|
{renderNicknameModal()}
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
|
|
|
container: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
backgroundColor: colors.background.default,
|
|
|
|
|
|
},
|
|
|
|
|
|
section: {
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
sectionHeader: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
paddingHorizontal: spacing.md,
|
|
|
|
|
|
paddingVertical: spacing.sm,
|
|
|
|
|
|
backgroundColor: colors.background.default,
|
|
|
|
|
|
},
|
|
|
|
|
|
memberItem: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
paddingHorizontal: spacing.md,
|
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
borderBottomWidth: 1,
|
|
|
|
|
|
borderBottomColor: colors.divider,
|
|
|
|
|
|
},
|
|
|
|
|
|
memberInfo: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
marginLeft: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
memberNameRow: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
marginBottom: 2,
|
|
|
|
|
|
},
|
|
|
|
|
|
memberName: {
|
|
|
|
|
|
fontWeight: '500',
|
|
|
|
|
|
},
|
|
|
|
|
|
roleBadge: {
|
|
|
|
|
|
paddingHorizontal: spacing.xs,
|
|
|
|
|
|
paddingVertical: 2,
|
|
|
|
|
|
borderRadius: borderRadius.sm,
|
|
|
|
|
|
marginLeft: spacing.sm,
|
|
|
|
|
|
},
|
|
|
|
|
|
mutedBadge: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
marginTop: 2,
|
|
|
|
|
|
},
|
|
|
|
|
|
// 模态框样式
|
|
|
|
|
|
modalOverlay: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
|
|
|
|
justifyContent: 'flex-end',
|
|
|
|
|
|
},
|
|
|
|
|
|
modalContent: {
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
borderTopLeftRadius: borderRadius.lg,
|
|
|
|
|
|
borderTopRightRadius: borderRadius.lg,
|
|
|
|
|
|
padding: spacing.lg,
|
|
|
|
|
|
maxHeight: '80%',
|
|
|
|
|
|
},
|
|
|
|
|
|
modalHeader: {
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
modalTitle: {
|
|
|
|
|
|
fontWeight: '700',
|
|
|
|
|
|
marginTop: spacing.sm,
|
|
|
|
|
|
marginBottom: spacing.xs,
|
|
|
|
|
|
},
|
|
|
|
|
|
actionItem: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
|
borderBottomWidth: 1,
|
|
|
|
|
|
borderBottomColor: colors.divider,
|
|
|
|
|
|
},
|
|
|
|
|
|
actionText: {
|
|
|
|
|
|
marginLeft: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
inputLabel: {
|
|
|
|
|
|
marginBottom: spacing.xs,
|
|
|
|
|
|
},
|
|
|
|
|
|
input: {
|
|
|
|
|
|
backgroundColor: colors.background.default,
|
|
|
|
|
|
borderRadius: borderRadius.md,
|
|
|
|
|
|
paddingHorizontal: spacing.md,
|
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
|
fontSize: fontSizes.md,
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
borderColor: colors.divider,
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
modalButtons: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
marginTop: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
modalButton: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
marginHorizontal: spacing.xs,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
export default GroupMembersScreen;
|