Files
frontend/src/screens/message/CreateGroupScreen.tsx

485 lines
14 KiB
TypeScript
Raw Normal View History

/**
* CreateGroupScreen
*
*
*/
import React, { useState, useMemo } from 'react';
import {
View,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
TextInput,
FlatList,
Image,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { spacing, fontSizes, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
import { groupService } from '../../services/groupService';
import { uploadService } from '../../services/uploadService';
import { Avatar, Text, Button, Loading } from '../../components/common';
import { User } from '../../types';
import MutualFollowSelectorModal from './components/MutualFollowSelectorModal';
const CreateGroupScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createCreateGroupStyles(colors), [colors]);
const router = useRouter();
// 表单状态
const [groupName, setGroupName] = useState('');
const [groupDescription, setGroupDescription] = useState('');
const [groupAvatar, setGroupAvatar] = useState<string | null>(null);
const [selectedMembers, setSelectedMembers] = useState<User[]>([]);
const [selectedMemberIds, setSelectedMemberIds] = useState<Set<string>>(new Set());
// 提交状态
const [submitting, setSubmitting] = useState(false);
const [uploadingAvatar, setUploadingAvatar] = useState(false);
// 邀请成员模态框状态
const [inviteModalVisible, setInviteModalVisible] = useState(false);
// 移除已选成员
const removeSelectedMember = (userId: string) => {
const newSelectedIds = new Set(selectedMemberIds);
newSelectedIds.delete(userId);
setSelectedMemberIds(newSelectedIds);
setSelectedMembers(prev => prev.filter(m => m.id !== userId));
};
// 选择群头像
const handleSelectAvatar = async () => {
try {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
Alert.alert('提示', '需要访问相册权限才能选择头像');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: 'images',
allowsEditing: true,
aspect: [1, 1],
quality: 0.8,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
const selectedAsset = result.assets[0];
setUploadingAvatar(true);
try {
const uploadResult = await uploadService.uploadGroupAvatar({
uri: selectedAsset.uri,
type: selectedAsset.mimeType || 'image/jpeg',
});
if (uploadResult && uploadResult.url) {
setGroupAvatar(uploadResult.url);
} else {
Alert.alert('上传失败', '头像上传失败,请重试');
}
} catch (error) {
console.error('上传头像失败:', error);
Alert.alert('上传失败', '头像上传失败,请重试');
} finally {
setUploadingAvatar(false);
}
}
} catch (error) {
console.error('选择头像失败:', error);
Alert.alert('错误', '选择头像时发生错误');
}
};
// 创建群组
const handleCreateGroup = async () => {
// 验证群名称
if (!groupName.trim()) {
Alert.alert('提示', '请输入群名称');
return;
}
if (groupName.length > 50) {
Alert.alert('提示', '群名称最多50个字符');
return;
}
if (groupDescription.length > 500) {
Alert.alert('提示', '群描述最多500个字符');
return;
}
setSubmitting(true);
try {
const memberIds = selectedMembers.map(m => m.id);
const response = await groupService.createGroup({
name: groupName.trim(),
description: groupDescription.trim() || undefined,
member_ids: memberIds.length > 0 ? memberIds : undefined,
});
// 如果设置了头像,更新群头像
if (groupAvatar && response.id) {
try {
await groupService.setGroupAvatar(response.id, groupAvatar);
} catch (avatarError) {
console.error('设置群头像失败:', avatarError);
}
}
Alert.alert('成功', '群组创建成功', [
{
text: '确定',
onPress: () => router.back(),
},
]);
} catch (error: any) {
console.error('创建群组失败:', error);
Alert.alert('创建失败', error.message || '创建群组时发生错误,请重试');
} finally {
setSubmitting(false);
}
};
// 渲染已选成员
const renderSelectedMember = ({ item }: { item: User }) => (
<View style={styles.selectedMemberItem}>
<Avatar source={item.avatar} size={48} name={item.nickname} />
<TouchableOpacity
style={styles.removeMemberButton}
onPress={() => removeSelectedMember(item.id)}
>
<View style={styles.removeIconContainer}>
<MaterialCommunityIcons name="close" size={12} color={colors.background.paper} />
</View>
</TouchableOpacity>
<Text variant="caption" numberOfLines={1} style={styles.selectedMemberName}>
{item.nickname}
</Text>
</View>
);
const handleConfirmMembers = (users: User[]) => {
setSelectedMembers(users);
setSelectedMemberIds(new Set(users.map(user => user.id)));
setInviteModalVisible(false);
};
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* 群头像和名称区域 */}
<View style={styles.headerSection}>
<View style={styles.avatarContainer}>
<TouchableOpacity style={styles.avatarWrapper} onPress={handleSelectAvatar} disabled={uploadingAvatar}>
{uploadingAvatar ? (
<View style={[styles.avatarPlaceholder, { width: 80, height: 80 }]}>
<Loading />
</View>
) : groupAvatar ? (
<Image source={{ uri: groupAvatar }} style={styles.avatarImage} />
) : (
<Avatar
source={undefined}
size={80}
name={groupName || '群'}
/>
)}
<View style={styles.avatarBadge}>
<MaterialCommunityIcons name="camera" size={14} color={colors.background.paper} />
</View>
</TouchableOpacity>
</View>
<View style={styles.nameInputContainer}>
<Text variant="label" style={styles.inputLabel}>
<Text color={colors.error.main}>*</Text>
</Text>
<TextInput
style={styles.nameInput}
value={groupName}
onChangeText={setGroupName}
placeholder="请输入群名称"
placeholderTextColor={colors.text.hint}
maxLength={50}
2026-03-16 17:47:10 +08:00
cursorColor={colors.primary.main}
selectionColor={`${colors.primary.main}40`}
/>
<Text variant="caption" color={colors.text.hint} style={styles.charCount}>
{groupName.length}/50
</Text>
</View>
</View>
{/* 群描述输入 */}
<View style={styles.section}>
<Text variant="label" style={styles.sectionTitle}>
</Text>
<View style={styles.textAreaContainer}>
<TextInput
style={styles.textArea}
value={groupDescription}
onChangeText={setGroupDescription}
placeholder="介绍一下这个群聊吧..."
placeholderTextColor={colors.text.hint}
maxLength={500}
multiline
numberOfLines={4}
textAlignVertical="top"
2026-03-16 17:47:10 +08:00
cursorColor={colors.primary.main}
selectionColor={`${colors.primary.main}40`}
/>
<Text variant="caption" color={colors.text.hint} style={styles.textAreaCharCount}>
{groupDescription.length}/500
</Text>
</View>
</View>
{/* 已选成员 */}
{selectedMembers.length > 0 && (
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text variant="label" style={styles.sectionTitle}>
</Text>
<Text variant="caption" color={colors.primary.main}>
{selectedMembers.length}
</Text>
</View>
<FlatList
data={selectedMembers}
renderItem={renderSelectedMember}
keyExtractor={item => item.id}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.selectedMembersList}
/>
</View>
)}
{/* 邀请成员按钮 */}
<TouchableOpacity
style={styles.inviteButton}
onPress={() => setInviteModalVisible(true)}
activeOpacity={0.8}
>
<View style={styles.inviteIconContainer}>
<MaterialCommunityIcons name="account-plus" size={24} color={colors.primary.main} />
</View>
<View style={styles.inviteTextContainer}>
<Text variant="body" style={styles.inviteTitle}></Text>
<Text variant="caption" color={colors.text.secondary}>
</Text>
</View>
<MaterialCommunityIcons name="chevron-right" size={24} color={colors.text.hint} />
</TouchableOpacity>
</ScrollView>
{/* 创建按钮 */}
<View style={styles.footer}>
<Button
title="创建群聊"
onPress={handleCreateGroup}
loading={submitting}
disabled={!groupName.trim() || submitting}
fullWidth
size="lg"
/>
</View>
<MutualFollowSelectorModal
visible={inviteModalVisible}
title="邀请成员"
confirmText="完成"
initialSelectedIds={Array.from(selectedMemberIds)}
onClose={() => setInviteModalVisible(false)}
onConfirm={handleConfirmMembers}
/>
</SafeAreaView>
);
};
function createCreateGroupStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: spacing.lg,
paddingBottom: spacing.xl,
},
// 头部区域样式
headerSection: {
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: spacing.xl,
},
avatarContainer: {
marginRight: spacing.lg,
},
avatarWrapper: {
position: 'relative',
},
avatarImage: {
width: 80,
height: 80,
borderRadius: 40,
},
avatarPlaceholder: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.background.default,
borderRadius: 40,
},
avatarBadge: {
position: 'absolute',
bottom: 0,
right: 0,
backgroundColor: colors.primary.main,
width: 28,
height: 28,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.background.paper,
},
nameInputContainer: {
flex: 1,
paddingTop: spacing.sm,
},
inputLabel: {
marginBottom: spacing.sm,
fontWeight: '600',
},
nameInput: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
fontSize: fontSizes.lg,
color: colors.text.primary,
borderWidth: 1,
borderColor: colors.divider,
},
charCount: {
textAlign: 'right',
marginTop: spacing.xs,
},
// 区域样式
section: {
marginBottom: spacing.xl,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.sm,
},
sectionTitle: {
fontWeight: '600',
marginBottom: spacing.sm,
},
// 文本域样式
textAreaContainer: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.divider,
padding: spacing.md,
},
textArea: {
minHeight: 100,
fontSize: fontSizes.md,
color: colors.text.primary,
lineHeight: 22,
},
textAreaCharCount: {
textAlign: 'right',
marginTop: spacing.sm,
},
// 已选成员样式
selectedMembersList: {
paddingVertical: spacing.sm,
},
selectedMemberItem: {
alignItems: 'center',
marginRight: spacing.lg,
width: 64,
},
removeMemberButton: {
position: 'absolute',
top: -4,
right: 4,
},
removeIconContainer: {
backgroundColor: colors.text.secondary,
borderRadius: 10,
width: 20,
height: 20,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: colors.background.paper,
},
selectedMemberName: {
marginTop: spacing.xs,
textAlign: 'center',
width: '100%',
},
// 邀请按钮样式
inviteButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.md,
marginBottom: spacing.xl,
...shadows.sm,
},
inviteIconContainer: {
width: 48,
height: 48,
borderRadius: borderRadius.lg,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
inviteTextContainer: {
flex: 1,
},
inviteTitle: {
fontWeight: '600',
marginBottom: 2,
},
// 底部按钮样式
footer: {
padding: spacing.lg,
paddingBottom: spacing.xl,
backgroundColor: colors.background.paper,
borderTopWidth: 1,
borderTopColor: colors.divider,
},
});
}
export default CreateGroupScreen;