Add support for referencing/quoting other posts within post content. Includes: - New `post_ref` segment type with `PostRefSegmentData` interface for post references - New `PostRefCard` component to display referenced posts inline - Post suggestion search triggered by `#` in `PostMentionInput` - Updated `PostContentRenderer` to render post reference segments - Add `suggestPosts` and `recordRefClick` API methods in postService Additional improvements: - Add image gallery preview for user profile covers and avatars - Make text selectable across CommentItem, PostContentRenderer, and PostDetailScreen - Migrate FlatList to FlashList in PostDetailScreen and UserProfileScreen - Add nestedScrollEnabled to CreatePostScreen - Add clickable member avatars in GroupMembersScreen
850 lines
25 KiB
TypeScript
850 lines
25 KiB
TypeScript
/**
|
||
* 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<ICursorGroupMemberListPagedSource>(() => {
|
||
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<GroupMemberResponse[]>([]);
|
||
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<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('');
|
||
|
||
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<GroupMemberResponse> = ({ item }) => {
|
||
const isSelf = item.user_id === currentUser?.id;
|
||
const canManage = isAdmin && item.role !== 'owner' && (isOwner || item.role === 'member');
|
||
|
||
return (
|
||
<View style={styles.memberItem}>
|
||
<TouchableOpacity
|
||
onPress={() => handleAvatarPress(item)}
|
||
activeOpacity={0.7}
|
||
>
|
||
<Avatar
|
||
source={item.user?.avatar}
|
||
size={48}
|
||
name={item.user?.nickname || item.nickname}
|
||
/>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity
|
||
style={styles.memberInfo}
|
||
onPress={() => isSelf ? openNicknameModal(item) : handleAvatarPress(item)}
|
||
onLongPress={() => isSelf ? openNicknameModal(item) : null}
|
||
activeOpacity={0.7}
|
||
>
|
||
<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>
|
||
)}
|
||
</TouchableOpacity>
|
||
{canManage && (
|
||
<TouchableOpacity
|
||
onPress={() => openActionModal(item)}
|
||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||
>
|
||
<MaterialCommunityIcons name="dots-vertical" size={20} color={colors.text.hint} />
|
||
</TouchableOpacity>
|
||
)}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
// 渲染分组头部
|
||
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 || isLoading) 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={isRefreshing}
|
||
onRefresh={onRefresh}
|
||
colors={[colors.primary.main]}
|
||
tintColor={colors.primary.main}
|
||
/>
|
||
}
|
||
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)}
|
||
{group.data.map((member, index) => (
|
||
<View key={member.id}>
|
||
{renderMember({ item: member, index } as any)}
|
||
</View>
|
||
))}
|
||
</View>
|
||
)}
|
||
/>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||
{/* Header */}
|
||
<View style={styles.pageHeader}>
|
||
<AppBackButton onPress={() => router.back()} />
|
||
<Text variant="h3" style={styles.pageTitle}>群成员</Text>
|
||
<View style={styles.pageHeaderPlaceholder} />
|
||
</View>
|
||
{loading ? <Loading /> : renderGroupedList()}
|
||
{renderActionModal()}
|
||
{renderNicknameModal()}
|
||
</SafeAreaView>
|
||
);
|
||
};
|
||
|
||
function createGroupMembersStyles(colors: AppColors) {
|
||
return StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: colors.background.default,
|
||
},
|
||
// Header 样式 - 扁平化
|
||
pageHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
paddingHorizontal: spacing.md,
|
||
paddingVertical: spacing.md,
|
||
backgroundColor: colors.background.paper,
|
||
borderBottomWidth: 0.5,
|
||
borderBottomColor: colors.divider + '60',
|
||
},
|
||
pageTitle: {
|
||
fontWeight: '800',
|
||
fontSize: fontSizes.xl + 1,
|
||
letterSpacing: 0.5,
|
||
},
|
||
pageHeaderPlaceholder: {
|
||
width: 40,
|
||
height: 40,
|
||
},
|
||
section: {
|
||
marginBottom: spacing.md,
|
||
},
|
||
sectionHeader: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
paddingHorizontal: spacing.lg,
|
||
paddingVertical: spacing.md,
|
||
backgroundColor: colors.background.default,
|
||
borderTopWidth: 0.5,
|
||
borderTopColor: colors.divider + '40',
|
||
},
|
||
memberItem: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingHorizontal: spacing.lg,
|
||
paddingVertical: spacing.md,
|
||
backgroundColor: colors.background.paper,
|
||
borderBottomWidth: 0.5,
|
||
borderBottomColor: colors.divider + '40',
|
||
},
|
||
memberInfo: {
|
||
flex: 1,
|
||
marginLeft: spacing.md,
|
||
},
|
||
memberNameRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
marginBottom: 2,
|
||
},
|
||
memberName: {
|
||
fontWeight: '700',
|
||
fontSize: fontSizes.md + 1,
|
||
letterSpacing: 0.3,
|
||
},
|
||
roleBadge: {
|
||
paddingHorizontal: spacing.sm,
|
||
paddingVertical: 2,
|
||
borderRadius: borderRadius.sm,
|
||
marginLeft: spacing.sm,
|
||
fontWeight: '800',
|
||
fontSize: fontSizes.xs,
|
||
color: colors.background.paper,
|
||
},
|
||
mutedBadge: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
marginTop: 2,
|
||
},
|
||
// 模态框样式 - 更现代
|
||
modalOverlay: {
|
||
flex: 1,
|
||
backgroundColor: 'rgba(0, 0, 0, 0.45)',
|
||
justifyContent: 'flex-end',
|
||
},
|
||
modalContent: {
|
||
backgroundColor: colors.background.paper,
|
||
borderTopLeftRadius: borderRadius['2xl'],
|
||
borderTopRightRadius: borderRadius['2xl'],
|
||
padding: spacing.lg,
|
||
maxHeight: '80%',
|
||
},
|
||
modalHeader: {
|
||
alignItems: 'center',
|
||
marginBottom: spacing.lg,
|
||
paddingBottom: spacing.md,
|
||
borderBottomWidth: 0.5,
|
||
borderBottomColor: colors.divider + '60',
|
||
},
|
||
modalTitle: {
|
||
fontWeight: '800',
|
||
fontSize: fontSizes.xl + 1,
|
||
marginTop: spacing.sm,
|
||
marginBottom: spacing.xs,
|
||
letterSpacing: 0.5,
|
||
},
|
||
actionItem: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingVertical: spacing.md,
|
||
paddingHorizontal: spacing.sm,
|
||
marginLeft: -spacing.sm,
|
||
marginRight: -spacing.sm,
|
||
borderRadius: borderRadius.md,
|
||
borderBottomWidth: 0.5,
|
||
borderBottomColor: colors.divider + '40',
|
||
},
|
||
actionText: {
|
||
marginLeft: spacing.md,
|
||
fontWeight: '600',
|
||
fontSize: fontSizes.md,
|
||
},
|
||
inputLabel: {
|
||
marginBottom: spacing.xs,
|
||
fontWeight: '700',
|
||
fontSize: fontSizes.sm + 1,
|
||
},
|
||
input: {
|
||
backgroundColor: colors.background.default,
|
||
borderRadius: borderRadius.lg,
|
||
paddingHorizontal: spacing.md,
|
||
paddingVertical: spacing.md,
|
||
fontSize: fontSizes.md,
|
||
color: colors.text.primary,
|
||
borderWidth: 1.5,
|
||
borderColor: colors.divider + '80',
|
||
marginBottom: spacing.md,
|
||
},
|
||
modalButtons: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
marginTop: spacing.md,
|
||
},
|
||
modalButton: {
|
||
flex: 1,
|
||
marginHorizontal: spacing.xs,
|
||
},
|
||
// 分页加载样式
|
||
loadingFooter: {
|
||
paddingVertical: spacing.md,
|
||
alignItems: 'center',
|
||
},
|
||
loadMoreBtn: {
|
||
paddingVertical: spacing.md,
|
||
alignItems: 'center',
|
||
},
|
||
noMoreText: {
|
||
textAlign: 'center',
|
||
paddingVertical: spacing.md,
|
||
fontWeight: '500',
|
||
color: colors.text.hint,
|
||
},
|
||
});
|
||
}
|
||
|
||
export default GroupMembersScreen;
|