import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react'; import { View, StyleSheet, TextInput, TouchableOpacity, Alert, ActivityIndicator, Clipboard, FlatList, RefreshControl, Animated, KeyboardAvoidingView, Platform, ScrollView, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme'; import Avatar from '../../components/common/Avatar'; import Text from '../../components/common/Text'; import { groupService } from '@/services/message'; import { groupManager } from '../../stores/group'; import { GroupResponse, JoinType } from '../../types/dto'; import { useCursorPagination } from '../../hooks/useCursorPagination'; import { EmptyState } from '../../components/common'; function createJoinGroupStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background.paper, }, keyboardView: { flex: 1, }, scrollContent: { flexGrow: 1, paddingHorizontal: 28, paddingTop: 40, paddingBottom: 40, }, content: { flex: 1, maxWidth: 400, width: '100%', alignSelf: 'center', }, // 标题区域 titleSection: { marginBottom: 32, marginTop: 12, }, title: { fontSize: 28, fontWeight: '700', color: colors.text.primary, lineHeight: 36, }, underline: { width: 40, height: 4, backgroundColor: colors.primary.main, borderRadius: 2, marginTop: 12, marginBottom: 12, }, subtitle: { fontSize: 15, color: colors.text.secondary, }, // 搜索区域 searchSection: { marginBottom: 24, }, inputLabel: { fontSize: 14, fontWeight: '500', color: colors.text.secondary, marginBottom: 10, }, searchRow: { flexDirection: 'row', alignItems: 'center', }, inputWrapper: { flex: 1, flexDirection: 'row', alignItems: 'center', backgroundColor: colors.background.default, borderRadius: 14, paddingHorizontal: 18, height: 56, borderWidth: 1, borderColor: 'transparent', }, input: { flex: 1, fontSize: 16, color: colors.text.primary, height: 56, }, searchBtn: { width: 56, height: 56, borderRadius: 14, backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', marginLeft: spacing.sm, }, searchBtnDisabled: { opacity: 0.5, }, // 搜索结果区域 searchResultSection: { marginBottom: 24, }, sectionTitle: { fontSize: 14, fontWeight: '500', color: colors.text.secondary, marginBottom: 12, }, emptyText: { textAlign: 'center', fontSize: 14, color: colors.text.hint, fontWeight: '500', paddingVertical: spacing.lg, }, // 群卡片 groupCard: { backgroundColor: colors.background.default, borderRadius: 16, padding: spacing.lg, marginBottom: spacing.md, }, groupHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: spacing.sm, }, groupMeta: { marginLeft: spacing.md, flex: 1, }, groupName: { fontWeight: '700', fontSize: 16, color: colors.text.primary, marginBottom: 2, }, groupDesc: { fontSize: 13, color: colors.text.secondary, lineHeight: 20, marginTop: spacing.sm, }, groupInfoRow: { marginTop: spacing.sm, marginBottom: spacing.xs, flexDirection: 'row', justifyContent: 'space-between', }, groupInfoText: { fontSize: 12, color: colors.text.secondary, }, groupNoRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: spacing.md, }, groupNoText: { fontSize: 12, color: colors.text.secondary, }, copyBtn: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: spacing.sm, paddingVertical: 4, borderRadius: borderRadius.sm, backgroundColor: colors.primary.light + '15', }, copyBtnText: { marginLeft: 4, fontSize: 12, fontWeight: '600', color: colors.primary.main, }, submitBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', borderRadius: 14, backgroundColor: colors.primary.main, height: 48, }, submitBtnDisabled: { opacity: 0.5, }, submitText: { marginLeft: spacing.xs, fontWeight: '700', fontSize: 15, color: colors.text.inverse, }, // 推荐区域 recommendSection: { flex: 1, }, loadingFooter: { paddingVertical: spacing.md, alignItems: 'center', }, loadMoreBtn: { paddingVertical: spacing.md, alignItems: 'center', }, loadMoreText: { fontSize: 14, fontWeight: '500', color: colors.primary.main, }, noMoreText: { textAlign: 'center', paddingVertical: spacing.md, fontWeight: '500', fontSize: 13, color: colors.text.hint, }, }); } const JoinGroupScreen: React.FC = () => { const colors = useAppColors(); const styles = useMemo(() => createJoinGroupStyles(colors), [colors]); const router = useRouter(); const [keyword, setKeyword] = useState(''); const [searching, setSearching] = useState(false); const [joiningGroupId, setJoiningGroupId] = useState(null); const [searchedGroup, setSearchedGroup] = useState(null); const [searched, setSearched] = useState(false); // 动画值 const fadeAnim = useRef(new Animated.Value(0)).current; const slideAnim = useRef(new Animated.Value(20)).current; useEffect(() => { Animated.parallel([ Animated.timing(fadeAnim, { toValue: 1, duration: 400, useNativeDriver: true, }), Animated.timing(slideAnim, { toValue: 0, duration: 400, useNativeDriver: true, }), ]).start(); }, []); // 使用游标分页 Hook 管理群组列表 const { list: groups, isLoading, isRefreshing, hasMore, loadMore, refresh, error, } = useCursorPagination( async ({ cursor, pageSize }) => { return await groupService.getGroupsCursor({ cursor, page_size: pageSize, }); }, { pageSize: 20 } ); const getJoinTypeText = (joinType: JoinType) => { if (joinType === 0) return '允许加入'; if (joinType === 1) return '需要审批'; return '禁止加入'; }; const handleSearch = async () => { const trimmed = keyword.trim(); if (!trimmed) { Alert.alert('提示', '请输入群ID进行搜索'); return; } setSearching(true); setSearched(true); try { const result = await groupManager.getGroup(trimmed, true); setSearchedGroup(result); } catch (error: any) { setSearchedGroup(null); const message = error?.response?.data?.message || error?.message || ''; if (String(message).includes('不存在') || error?.response?.status === 404) { Alert.alert('未找到', '未搜索到该群聊,请确认群ID是否正确'); } else { Alert.alert('搜索失败', '请稍后重试'); } } finally { setSearching(false); } }; const handleJoin = async (group: GroupResponse) => { if (!group?.id) return; setJoiningGroupId(String(group.id)); try { await groupService.joinGroup(group.id); Alert.alert('成功', '操作已提交', [ { text: '确定', onPress: () => router.back(), }, ]); } catch (error: any) { const message = error?.response?.data?.message || error?.message || '操作失败,请稍后重试'; Alert.alert('操作失败', String(message)); } finally { setJoiningGroupId(null); } }; const handleCopyGroupId = (groupId: string) => { Clipboard.setString(groupId); Alert.alert('已复制', '群号已复制到剪贴板'); }; const formatGroupNo = (id: string) => { const raw = id; if (raw.length <= 12) return raw; return `${raw.slice(0, 6)}...${raw.slice(-4)}`; }; const renderGroupItem = ({ item: group }: { item: GroupResponse }) => { const isJoining = joiningGroupId === String(group.id); return ( {group.name} {!!group.description && ( {group.description} )} 成员 {group.member_count}/{group.max_members} {getJoinTypeText(group.join_type)} 群号:{formatGroupNo(group.id)} handleCopyGroupId(group.id)}> 复制 handleJoin(group)} disabled={isJoining} > {isJoining ? ( ) : ( <> 申请入群 )} ); }; const renderEmptyList = () => { if (isLoading) return null; return ( ); }; const renderSearchResult = () => { if (!searched) return null; if (searchedGroup) { return ( 搜索结果 {renderGroupItem({ item: searchedGroup })} ); } if (!searching) { return ( 搜索结果 暂无搜索结果,请检查群ID后重试 ); } return null; }; return ( } > {/* 标题区域 */} 加入群聊 搜索群ID或浏览推荐群组 {/* 搜索区域 */} 搜索群聊(群ID) {searching ? ( ) : ( )} {/* 搜索结果 */} {renderSearchResult()} {/* 推荐群组 */} 推荐群组 {groups.length === 0 && !isLoading ? ( renderEmptyList() ) : ( <> {groups.map((group) => ( {renderGroupItem({ item: group })} ))} {isLoading && ( )} {hasMore && !isLoading && ( 加载更多 )} {!hasMore && groups.length > 0 && ( 没有更多群组了 )} )} ); }; export default JoinGroupScreen;