Files
frontend/src/screens/message/GroupInviteDetailScreen.tsx
lan 3968660048 Initial frontend repository commit.
Include app source and update .gitignore to exclude local release artifacts and signing files.

Made-with: Cursor
2026-03-09 21:29:03 +08:00

232 lines
7.2 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import { View, StyleSheet, ActivityIndicator, Alert, ScrollView } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { SafeAreaView } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { Avatar, Text } from '../../components/common';
import { RootStackParamList } from '../../navigation/types';
import { groupService } from '../../services/groupService';
import { groupManager } from '../../stores/groupManager';
import { GroupMemberResponse } from '../../types/dto';
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
type Route = RouteProp<RootStackParamList, 'GroupInviteDetail'>;
type Navigation = NativeStackNavigationProp<RootStackParamList>;
const GroupInviteDetailScreen: React.FC = () => {
const route = useRoute<Route>();
const navigation = useNavigation<Navigation>();
const { message } = route.params;
const { extra_data } = message;
const [loadingMembers, setLoadingMembers] = useState(false);
const [members, setMembers] = useState<GroupMemberResponse[]>([]);
const [submitting, setSubmitting] = useState(false);
const [memberCount, setMemberCount] = useState<number | null>(null);
const [loadingGroup, setLoadingGroup] = useState(false);
const canAction = extra_data?.request_status === 'pending';
useEffect(() => {
const loadGroup = async () => {
if (!extra_data?.group_id) return;
setLoadingGroup(true);
try {
const group = await groupManager.getGroup(extra_data.group_id);
setMemberCount(group.member_count ?? null);
} catch {
setMemberCount(null);
} finally {
setLoadingGroup(false);
}
};
loadGroup();
}, [extra_data?.group_id]);
useEffect(() => {
const loadMembers = async () => {
if (!extra_data?.group_id) return;
setLoadingMembers(true);
try {
const res = await groupManager.getMembers(extra_data.group_id, 1, 100);
const admins = (res.list || []).filter(m => m.role === 'owner' || m.role === 'admin');
setMembers(admins);
} catch {
setMembers([]);
} finally {
setLoadingMembers(false);
}
};
loadMembers();
}, [extra_data?.group_id]);
const handleDecision = async (approve: boolean) => {
const flag = extra_data?.flag;
if (!flag) {
Alert.alert('提示', '缺少邀请标识,无法处理');
return;
}
setSubmitting(true);
try {
await groupService.respondInvite({ flag, approve });
Alert.alert('成功', approve ? '已同意加入群聊' : '已拒绝邀请', [
{ text: '确定', onPress: () => navigation.goBack() },
]);
} catch {
Alert.alert('操作失败', '请稍后重试');
} finally {
setSubmitting(false);
}
};
const groupNo = useMemo(() => extra_data?.group_id || '-', [extra_data?.group_id]);
const formatGroupNo = (id: string) => {
if (id.length <= 12) return id;
return `${id.slice(0, 6)}...${id.slice(-4)}`;
};
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
<GroupInfoSummaryCard
groupName={extra_data?.group_name}
groupAvatar={extra_data?.group_avatar}
groupNo={formatGroupNo(groupNo)}
groupDescription={extra_data?.group_description}
memberCountText={loadingGroup ? '人数加载中...' : `${memberCount ?? '-'}`}
/>
<View style={styles.card}>
<View style={styles.cardHeader}>
<View style={styles.cardIconContainer}>
<MaterialCommunityIcons name="account-multiple" size={18} color={colors.info.main} />
</View>
<Text variant="label" style={styles.cardTitle}></Text>
<Text variant="caption" color={colors.text.secondary} style={styles.memberCount}>
{members.length}
</Text>
</View>
{loadingMembers ? (
<View style={styles.loadingWrap}>
<ActivityIndicator color={colors.primary.main} />
</View>
) : (
<View style={styles.memberPreview}>
{members.slice(0, 12).map((member, index) => (
<View
key={member.id}
style={[
styles.memberAvatar,
index === 0 && styles.memberAvatarFirst,
{ zIndex: index + 1 },
]}
>
<Avatar
source={member.user?.avatar || ''}
size={44}
name={member.user?.nickname || member.nickname}
/>
{member.role === 'owner' && (
<View style={styles.ownerBadge}>
<Text variant="caption" color={colors.background.paper} style={styles.ownerBadgeText}></Text>
</View>
)}
</View>
))}
{members.length === 0 && (
<Text variant="caption" color={colors.text.secondary}>
</Text>
)}
</View>
)}
</View>
</ScrollView>
<DecisionFooter
canAction={canAction}
submitting={submitting}
onReject={() => handleDecision(false)}
onApprove={() => handleDecision(true)}
processedText="该邀请已处理"
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: spacing.lg,
paddingBottom: spacing.xl,
},
card: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.lg,
...shadows.sm,
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.md,
},
cardIconContainer: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: colors.info.light + '30',
alignItems: 'center',
justifyContent: 'center',
marginRight: spacing.sm,
},
cardTitle: {
fontWeight: '600',
flex: 1,
},
memberCount: {
marginRight: spacing.xs,
},
loadingWrap: {
paddingVertical: spacing.md,
alignItems: 'center',
},
memberPreview: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
},
memberAvatar: {
marginLeft: -8,
marginBottom: spacing.sm,
},
memberAvatarFirst: {
marginLeft: 0,
},
ownerBadge: {
position: 'absolute',
bottom: -2,
right: -2,
backgroundColor: colors.warning.main,
borderRadius: 8,
paddingHorizontal: 4,
paddingVertical: 1,
},
ownerBadgeText: {
fontSize: 10,
fontWeight: '700',
},
});
export default GroupInviteDetailScreen;