/** * GroupInfoPanel.web.tsx * Web端群信息侧边栏组件 * 大屏幕模式下从右侧滑入显示 * 实现手机端群信息界面的核心功能 */ import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { View, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Animated, Alert, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Avatar, Text } from '../../../../components/common'; import { useAppColors, spacing, fontSizes, shadows, type AppColors } from '../../../../theme'; import { GroupMemberResponse, GroupResponse, GroupAnnouncementResponse } from '../../../../types/dto'; import { useBreakpointGTE } from '../../../../hooks/useResponsive'; import { groupManager } from '../../../../stores/groupManager'; import { groupService } from '../../../../services/groupService'; const { width: screenWidth } = Dimensions.get('window'); const PANEL_WIDTH = 360; interface GroupInfoPanelProps { visible: boolean; groupId?: string; groupInfo: GroupResponse | null; conversationId?: string; isPinned?: boolean; currentUserId?: string; onClose: () => void; onTogglePin?: (pinned: boolean) => void; onLeaveGroup?: () => void; onInviteMembers?: () => void; onViewAllMembers?: () => void; } export const GroupInfoPanel: React.FC = ({ visible, groupId, groupInfo, conversationId, isPinned, currentUserId, onClose, onTogglePin, onLeaveGroup, onInviteMembers, onViewAllMembers, }) => { const colors = useAppColors(); const styles = useMemo(() => createGroupInfoPanelStyles(colors), [colors]); const isWideScreen = useBreakpointGTE('lg'); // Web 端不使用 useNativeDriver const slideAnim = React.useRef(new Animated.Value(PANEL_WIDTH)).current; const fadeAnim = React.useRef(new Animated.Value(0)).current; const [members, setMembers] = useState([]); const [announcements, setAnnouncements] = useState([]); const [loading, setLoading] = useState(false); const [showAllMembers, setShowAllMembers] = useState(false); const [allMembers, setAllMembers] = useState([]); const [loadingMoreMembers, setLoadingMoreMembers] = useState(false); // 加载群成员(初始只加载10个) const loadMembers = useCallback(async () => { if (!groupId) return; try { setLoading(true); const response = await groupManager.getMembers(groupId, 1, 10); setMembers(response.list || []); } catch (error) { console.error('[GroupInfoPanel] 加载成员失败:', error); } finally { setLoading(false); } }, [groupId]); // 加载全部群成员 const loadAllMembers = useCallback(async () => { if (!groupId) return; try { setLoadingMoreMembers(true); const response = await groupManager.getMembers(groupId, 1, 100); setAllMembers(response.list || []); setShowAllMembers(true); } catch (error) { console.error('[GroupInfoPanel] 加载全部成员失败:', error); } finally { setLoadingMoreMembers(false); } }, [groupId]); // 加载群公告 const loadAnnouncements = useCallback(async () => { if (!groupId) return; try { const response = await groupService.getAnnouncements(groupId, 1, 10); setAnnouncements(response.list || []); } catch (error) { console.error('[GroupInfoPanel] 加载公告失败:', error); } }, [groupId]); // 每次打开面板时重置状态 useEffect(() => { if (visible) { setShowAllMembers(false); setAllMembers([]); } }, [visible]); useEffect(() => { if (visible && groupId) { loadMembers(); loadAnnouncements(); } }, [visible, groupId, loadMembers, loadAnnouncements]); useEffect(() => { if (visible) { Animated.parallel([ Animated.timing(slideAnim, { toValue: 0, duration: 300, // Web 端不使用 useNativeDriver useNativeDriver: false, }), Animated.timing(fadeAnim, { toValue: 1, duration: 300, // Web 端不使用 useNativeDriver useNativeDriver: false, }), ]).start(); } else { Animated.parallel([ Animated.timing(slideAnim, { toValue: PANEL_WIDTH, duration: 300, // Web 端不使用 useNativeDriver useNativeDriver: false, }), Animated.timing(fadeAnim, { toValue: 0, duration: 300, // Web 端不使用 useNativeDriver useNativeDriver: false, }), ]).start(); } }, [visible, slideAnim, fadeAnim]); if (!isWideScreen) { return null; } // 获取加群方式文本 const getJoinTypeText = (joinType?: number): string => { switch (joinType) { case 0: return '允许任何人加入'; case 1: return '需要管理员审批'; case 2: return '不允许任何人加入'; default: return '未知'; } }; return ( <> {/* 遮罩层 */} {visible && ( )} {/* 侧边栏面板 */} {/* 头部 */} 群信息 {/* 群头像和名称 */} {groupInfo?.name || '群聊'} {groupInfo?.member_count || members.length || 0} 位成员 {groupInfo?.join_type !== undefined && ( {getJoinTypeText(groupInfo.join_type)} )} {/* 邀请新成员按钮 */} {onInviteMembers && ( 邀请新成员 )} {/* 分割线 */} {/* 群公告 */} {announcements.length > 0 && ( 群公告 {announcements[0].content} {new Date(announcements[0].created_at).toLocaleDateString()} )} {/* 群简介 */} {!!groupInfo?.description && ( 群简介 {groupInfo.description} )} {/* 群成员列表 */} 成员列表 ({groupInfo?.member_count || members.length}) {onViewAllMembers && !showAllMembers && members.length >= 10 && ( {loadingMoreMembers ? '加载中...' : '查看全部'} )} {onViewAllMembers && showAllMembers && ( setShowAllMembers(false)} style={styles.viewAllButton}> 收起 )} {(showAllMembers ? allMembers : members).map((member) => ( {member.nickname || member.user?.nickname || '用户'} {member.role === 'owner' && ( 群主 )} {member.role === 'admin' && ( 管理员 )} ))} {/* 群信息 */} 群信息 群号 {groupInfo?.id || '-'} 创建时间 {groupInfo?.created_at ? new Date(groupInfo.created_at).toLocaleDateString('zh-CN') : '-'} 全员禁言 {groupInfo?.mute_all ? '已开启' : '已关闭'} {/* 操作按钮 */} {conversationId && onTogglePin && ( onTogglePin(!isPinned)} activeOpacity={0.7} > {isPinned ? '取消置顶' : '置顶群聊'} )} {onLeaveGroup && ( 退出群聊 )} ); }; function createGroupInfoPanelStyles(colors: AppColors) { return StyleSheet.create({ overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.35)', zIndex: 100, }, overlayTouchable: { flex: 1, }, panel: { position: 'absolute', right: 0, top: 0, bottom: 0, width: PANEL_WIDTH, backgroundColor: colors.background.paper, zIndex: 101, borderLeftWidth: 0.5, borderLeftColor: colors.divider + '60', }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: spacing.lg, paddingVertical: spacing.md, borderBottomWidth: 0.5, borderBottomColor: colors.divider + '60', backgroundColor: colors.background.paper, }, headerTitle: { fontSize: fontSizes.xl + 1, fontWeight: '800', color: colors.text.primary, letterSpacing: 0.5, }, closeButton: { padding: spacing.xs, borderRadius: borderRadius.md, }, content: { flex: 1, }, groupInfoSection: { alignItems: 'center', paddingVertical: spacing.xl, paddingHorizontal: spacing.md, }, groupName: { fontSize: fontSizes.xl + 1, fontWeight: '800', color: colors.text.primary, marginTop: spacing.md, letterSpacing: 0.3, }, memberCount: { fontSize: fontSizes.md, color: colors.text.secondary, marginTop: spacing.xs, fontWeight: '500', }, joinType: { fontSize: fontSizes.sm, color: colors.primary.main, marginTop: spacing.xs, fontWeight: '600', }, inviteButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: spacing.sm, paddingHorizontal: spacing.md, backgroundColor: colors.primary.light + '12', borderRadius: borderRadius.lg, marginTop: spacing.md, borderWidth: 1.5, borderColor: colors.primary.light + '60', borderStyle: 'dashed', }, inviteButtonText: { fontSize: fontSizes.sm, color: colors.primary.main, marginLeft: spacing.xs, fontWeight: '700', letterSpacing: 0.3, }, divider: { height: 0.5, backgroundColor: colors.divider + '60', marginHorizontal: spacing.lg, }, section: { padding: spacing.lg, }, sectionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: spacing.sm, }, sectionTitle: { fontSize: fontSizes.md + 1, fontWeight: '700', color: colors.text.primary, marginLeft: spacing.xs, letterSpacing: 0.3, }, noticeBox: { backgroundColor: colors.warning.light + '08', padding: spacing.md, borderRadius: borderRadius.lg, borderLeftWidth: 3, borderLeftColor: colors.warning.main, }, noticeText: { fontSize: fontSizes.sm, color: colors.text.secondary, lineHeight: 22, fontWeight: '400', }, noticeTime: { fontSize: fontSizes.xs, color: colors.text.hint, marginTop: spacing.xs, textAlign: 'right', fontWeight: '500', }, descriptionBox: { backgroundColor: colors.background.default, padding: spacing.md, borderRadius: borderRadius.lg, borderWidth: 0.5, borderColor: colors.divider + '30', }, description: { fontSize: fontSizes.sm, color: colors.text.secondary, lineHeight: 22, fontWeight: '400', }, memberList: { gap: spacing.sm, }, memberListHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: spacing.sm, }, viewAllButton: { flexDirection: 'row', alignItems: 'center', }, viewAllText: { fontSize: fontSizes.sm, color: colors.primary.main, fontWeight: '600', }, memberItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: spacing.sm, paddingHorizontal: spacing.sm, borderRadius: borderRadius.md, marginLeft: -spacing.sm, marginRight: -spacing.sm, }, memberInfo: { flex: 1, marginLeft: spacing.md, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, memberName: { fontSize: fontSizes.md, color: colors.text.primary, flex: 1, fontWeight: '600', }, ownerBadge: { fontSize: fontSizes.xs, color: colors.background.paper, marginLeft: spacing.xs, backgroundColor: colors.warning.main, paddingHorizontal: 6, paddingVertical: 2, borderRadius: borderRadius.sm, fontWeight: '800', }, adminBadge: { fontSize: fontSizes.xs, color: colors.primary.main, marginLeft: spacing.xs, backgroundColor: colors.primary.light + '20', paddingHorizontal: 6, paddingVertical: 2, borderRadius: borderRadius.sm, fontWeight: '700', }, moreMembers: { fontSize: fontSizes.sm, color: colors.text.secondary, textAlign: 'center', paddingVertical: spacing.sm, fontWeight: '500', }, infoRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: spacing.md, borderBottomWidth: 0.5, borderBottomColor: colors.divider + '40', }, infoLabel: { fontSize: fontSizes.sm, color: colors.text.secondary, fontWeight: '500', }, infoValue: { fontSize: fontSizes.sm, color: colors.text.primary, fontWeight: '600', textAlign: 'right', }, actionSection: { padding: spacing.lg, paddingBottom: spacing.xl, }, actionButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: spacing.md, backgroundColor: colors.primary.light + '10', borderRadius: borderRadius.lg, marginBottom: spacing.sm, borderWidth: 1.5, borderColor: colors.primary.light + '50', }, actionButtonText: { fontSize: fontSizes.md, fontWeight: '700', color: colors.primary.main, marginLeft: spacing.sm, letterSpacing: 0.3, }, leaveButton: { backgroundColor: colors.error.light + '08', borderColor: colors.error.light + '50', borderWidth: 1.5, }, leaveButtonText: { color: colors.error.main, fontWeight: '700', }, }); } export default GroupInfoPanel;