Files
frontend/src/screens/message/components/ChatScreen/GroupInfoPanel.tsx
lafay 4ee3079b9f
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 8m15s
Frontend CI / ota-android (push) Successful in 10m56s
Frontend CI / build-android-apk (push) Successful in 1h3m22s
feat(Theme): enhance theming and UI consistency across components
- Updated app.json to set userInterfaceStyle to automatic for improved theme adaptability.
- Refactored layout components to utilize useAppColors for dynamic theming, ensuring consistent color usage.
- Introduced SystemChrome component to manage system UI background color based on theme.
- Enhanced TabsLayout, ProfileStackLayout, and other components to leverage new theming structure.
- Improved QRCodeScanner, SearchBar, and CommentItem styles to align with the updated theme system.
- Consolidated styles in SystemMessageItem and TabBar for better maintainability and visual coherence.
2026-03-25 05:16:54 +08:00

598 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* GroupInfoPanel.tsx
* 群信息侧边栏组件
* 大屏幕模式下从右侧滑入显示
* 实现手机端群信息界面的核心功能
*/
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<GroupInfoPanelProps> = ({
visible,
groupId,
groupInfo,
conversationId,
isPinned,
currentUserId,
onClose,
onTogglePin,
onLeaveGroup,
onInviteMembers,
onViewAllMembers,
}) => {
const colors = useAppColors();
const styles = useMemo(() => createGroupInfoPanelStyles(colors), [colors]);
const isWideScreen = useBreakpointGTE('lg');
const slideAnim = React.useRef(new Animated.Value(PANEL_WIDTH)).current;
const fadeAnim = React.useRef(new Animated.Value(0)).current;
const [members, setMembers] = useState<GroupMemberResponse[]>([]);
const [announcements, setAnnouncements] = useState<GroupAnnouncementResponse[]>([]);
const [loading, setLoading] = useState(false);
const [showAllMembers, setShowAllMembers] = useState(false);
const [allMembers, setAllMembers] = useState<GroupMemberResponse[]>([]);
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,
useNativeDriver: true,
}),
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
]).start();
} else {
Animated.parallel([
Animated.timing(slideAnim, {
toValue: PANEL_WIDTH,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(fadeAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}),
]).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 && (
<Animated.View style={[styles.overlay, { opacity: fadeAnim }]}>
<TouchableOpacity
style={styles.overlayTouchable}
onPress={onClose}
activeOpacity={1}
/>
</Animated.View>
)}
{/* 侧边栏面板 */}
<Animated.View
style={[
styles.panel,
{
transform: [{ translateX: slideAnim }],
},
]}
>
{/* 头部 */}
<View style={styles.header}>
<Text style={styles.headerTitle}></Text>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<MaterialCommunityIcons name="close" size={24} color="#333" />
</TouchableOpacity>
</View>
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
{/* 群头像和名称 */}
<View style={styles.groupInfoSection}>
<Avatar
source={groupInfo?.avatar}
size={80}
name={groupInfo?.name || '群聊'}
/>
<Text style={styles.groupName}>{groupInfo?.name || '群聊'}</Text>
<Text style={styles.memberCount}>
{groupInfo?.member_count || members.length || 0}
</Text>
{groupInfo?.join_type !== undefined && (
<Text style={styles.joinType}>
{getJoinTypeText(groupInfo.join_type)}
</Text>
)}
{/* 邀请新成员按钮 */}
{onInviteMembers && (
<TouchableOpacity
style={styles.inviteButton}
onPress={onInviteMembers}
activeOpacity={0.7}
>
<MaterialCommunityIcons name="account-plus" size={18} color={colors.primary.main} />
<Text style={styles.inviteButtonText}></Text>
</TouchableOpacity>
)}
</View>
{/* 分割线 */}
<View style={styles.divider} />
{/* 群公告 */}
{announcements.length > 0 && (
<View style={styles.section}>
<View style={styles.sectionHeader}>
<MaterialCommunityIcons name="bullhorn" size={18} color={colors.warning.main} />
<Text style={styles.sectionTitle}></Text>
</View>
<View style={styles.noticeBox}>
<Text style={styles.noticeText}>{announcements[0].content}</Text>
<Text style={styles.noticeTime}>
{new Date(announcements[0].created_at).toLocaleDateString()}
</Text>
</View>
</View>
)}
{/* 群简介 */}
{groupInfo?.description && (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.descriptionBox}>
<Text style={styles.description}>{groupInfo.description}</Text>
</View>
</View>
)}
{/* 群成员列表 */}
<View style={styles.section}>
<View style={styles.memberListHeader}>
<Text style={styles.sectionTitle}> ({groupInfo?.member_count || members.length})</Text>
{onViewAllMembers && !showAllMembers && members.length >= 10 && (
<TouchableOpacity onPress={loadAllMembers} style={styles.viewAllButton} disabled={loadingMoreMembers}>
<Text style={styles.viewAllText}>
{loadingMoreMembers ? '加载中...' : '查看全部'}
</Text>
<MaterialCommunityIcons name="chevron-right" size={16} color={colors.primary.main} />
</TouchableOpacity>
)}
{onViewAllMembers && showAllMembers && (
<TouchableOpacity onPress={() => setShowAllMembers(false)} style={styles.viewAllButton}>
<Text style={styles.viewAllText}></Text>
<MaterialCommunityIcons name="chevron-up" size={16} color={colors.primary.main} />
</TouchableOpacity>
)}
</View>
<View style={styles.memberList}>
{(showAllMembers ? allMembers : members).map((member) => (
<View key={member.id || member.user_id} style={styles.memberItem}>
<Avatar
source={member.user?.avatar}
size={44}
name={member.nickname || member.user?.nickname || '用户'}
/>
<View style={styles.memberInfo}>
<Text style={styles.memberName} numberOfLines={1}>
{member.nickname || member.user?.nickname || '用户'}
</Text>
{member.role === 'owner' && (
<Text style={styles.ownerBadge}></Text>
)}
{member.role === 'admin' && (
<Text style={styles.adminBadge}></Text>
)}
</View>
</View>
))}
</View>
</View>
{/* 群信息 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>{groupInfo?.id || '-'}</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>
{groupInfo?.created_at
? new Date(groupInfo.created_at).toLocaleDateString('zh-CN')
: '-'}
</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}></Text>
<Text style={styles.infoValue}>
{groupInfo?.mute_all ? '已开启' : '已关闭'}
</Text>
</View>
</View>
{/* 操作按钮 */}
<View style={styles.actionSection}>
{conversationId && onTogglePin && (
<TouchableOpacity
style={styles.actionButton}
onPress={() => onTogglePin(!isPinned)}
activeOpacity={0.7}
>
<MaterialCommunityIcons
name={isPinned ? "pin-off" : "pin"}
size={22}
color={colors.primary.main}
/>
<Text style={styles.actionButtonText}>
{isPinned ? '取消置顶' : '置顶群聊'}
</Text>
</TouchableOpacity>
)}
{onLeaveGroup && (
<TouchableOpacity
style={[styles.actionButton, styles.leaveButton]}
onPress={onLeaveGroup}
activeOpacity={0.7}
>
<MaterialCommunityIcons
name="logout"
size={22}
color={colors.error.main}
/>
<Text style={[styles.actionButtonText, styles.leaveButtonText]}>
退
</Text>
</TouchableOpacity>
)}
</View>
</ScrollView>
</Animated.View>
</>
);
};
function createGroupInfoPanelStyles(colors: AppColors) {
return StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
zIndex: 100,
},
overlayTouchable: {
flex: 1,
},
panel: {
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
width: PANEL_WIDTH,
backgroundColor: colors.background.paper,
zIndex: 101,
...shadows.lg,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
headerTitle: {
fontSize: fontSizes.xl,
fontWeight: '600',
color: colors.text.primary,
},
closeButton: {
padding: spacing.xs,
},
content: {
flex: 1,
},
groupInfoSection: {
alignItems: 'center',
paddingVertical: spacing.xl,
},
groupName: {
fontSize: fontSizes.xl,
fontWeight: '600',
color: colors.text.primary,
marginTop: spacing.md,
},
memberCount: {
fontSize: fontSizes.md,
color: colors.text.secondary,
marginTop: spacing.xs,
},
joinType: {
fontSize: fontSizes.sm,
color: colors.primary.main,
marginTop: spacing.xs,
},
inviteButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
backgroundColor: colors.primary.light + '15',
borderRadius: 20,
marginTop: spacing.md,
borderWidth: 1,
borderColor: colors.primary.light,
},
inviteButtonText: {
fontSize: fontSizes.sm,
color: colors.primary.main,
marginLeft: spacing.xs,
fontWeight: '500',
},
divider: {
height: 1,
backgroundColor: colors.divider,
marginHorizontal: spacing.md,
},
section: {
padding: spacing.md,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.sm,
},
sectionTitle: {
fontSize: fontSizes.md,
fontWeight: '600',
color: colors.text.primary,
marginLeft: spacing.xs,
},
noticeBox: {
backgroundColor: colors.chat.tipBg,
padding: spacing.md,
borderRadius: 8,
borderLeftWidth: 3,
borderLeftColor: colors.warning.main,
},
noticeText: {
fontSize: fontSizes.sm,
color: colors.text.secondary,
lineHeight: 20,
},
noticeTime: {
fontSize: fontSizes.xs,
color: colors.text.hint,
marginTop: spacing.xs,
textAlign: 'right',
},
descriptionBox: {
backgroundColor: colors.chat.surfaceMuted,
padding: spacing.md,
borderRadius: 8,
},
description: {
fontSize: fontSizes.sm,
color: colors.text.secondary,
lineHeight: 20,
},
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,
},
memberItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.xs,
},
memberInfo: {
flex: 1,
marginLeft: spacing.sm,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
memberName: {
fontSize: fontSizes.md,
color: colors.text.primary,
flex: 1,
},
ownerBadge: {
fontSize: fontSizes.xs,
color: colors.warning.main,
marginLeft: spacing.xs,
backgroundColor: colors.warning.light + '30',
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
},
adminBadge: {
fontSize: fontSizes.xs,
color: colors.primary.main,
marginLeft: spacing.xs,
backgroundColor: colors.primary.light + '30',
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
},
moreMembers: {
fontSize: fontSizes.sm,
color: colors.text.secondary,
textAlign: 'center',
paddingVertical: spacing.sm,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: spacing.sm,
borderBottomWidth: 1,
borderBottomColor: colors.chat.borderHairline,
},
infoLabel: {
fontSize: fontSizes.sm,
color: colors.text.secondary,
},
infoValue: {
fontSize: fontSizes.sm,
color: colors.text.primary,
},
actionSection: {
padding: spacing.md,
paddingBottom: spacing.xl,
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
backgroundColor: colors.primary.light + '15',
borderRadius: 12,
marginBottom: spacing.sm,
borderWidth: 1,
borderColor: colors.primary.light,
},
actionButtonText: {
fontSize: fontSizes.md,
fontWeight: '600',
color: colors.primary.main,
marginLeft: spacing.sm,
},
leaveButton: {
backgroundColor: colors.error.light + '15',
borderColor: colors.error.light,
},
leaveButtonText: {
color: colors.error.main,
},
});
}
export default GroupInfoPanel;