2026-03-09 21:29:03 +08:00
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
|
import { View, StyleSheet, TextInput, TouchableOpacity, Alert, ActivityIndicator, Clipboard } from 'react-native';
|
|
|
|
|
|
import { useNavigation } from '@react-navigation/native';
|
|
|
|
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
|
|
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
|
|
|
|
|
|
|
|
|
|
import { colors, spacing, borderRadius } from '../../theme';
|
|
|
|
|
|
import Avatar from '../../components/common/Avatar';
|
|
|
|
|
|
import Text from '../../components/common/Text';
|
|
|
|
|
|
import { groupService } from '../../services/groupService';
|
|
|
|
|
|
import { groupManager } from '../../stores/groupManager';
|
|
|
|
|
|
import { RootStackParamList } from '../../navigation/types';
|
|
|
|
|
|
import { GroupResponse, JoinType } from '../../types/dto';
|
|
|
|
|
|
|
|
|
|
|
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
|
|
|
|
|
|
|
|
|
|
|
const JoinGroupScreen: React.FC = () => {
|
|
|
|
|
|
const navigation = useNavigation<NavigationProp>();
|
|
|
|
|
|
const [keyword, setKeyword] = useState('');
|
|
|
|
|
|
const [searching, setSearching] = useState(false);
|
|
|
|
|
|
const [joining, setJoining] = useState(false);
|
|
|
|
|
|
const [group, setGroup] = useState<GroupResponse | null>(null);
|
|
|
|
|
|
const [searched, setSearched] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
setGroup(result);
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
setGroup(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 () => {
|
|
|
|
|
|
if (!group?.id) return;
|
|
|
|
|
|
setJoining(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await groupService.joinGroup(group.id);
|
|
|
|
|
|
Alert.alert('成功', '操作已提交', [
|
|
|
|
|
|
{
|
|
|
|
|
|
text: '确定',
|
|
|
|
|
|
onPress: () => navigation.goBack(),
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
const message =
|
|
|
|
|
|
error?.response?.data?.message ||
|
|
|
|
|
|
error?.message ||
|
|
|
|
|
|
'操作失败,请稍后重试';
|
|
|
|
|
|
Alert.alert('操作失败', String(message));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setJoining(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleCopyGroupId = () => {
|
|
|
|
|
|
if (!group?.id) return;
|
|
|
|
|
|
Clipboard.setString(String(group.id));
|
|
|
|
|
|
Alert.alert('已复制', '群号已复制到剪贴板');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const formatGroupNo = (id: string | number) => {
|
|
|
|
|
|
const raw = String(id);
|
|
|
|
|
|
if (raw.length <= 12) return raw;
|
|
|
|
|
|
return `${raw.slice(0, 6)}...${raw.slice(-4)}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<View style={styles.container}>
|
|
|
|
|
|
<View style={styles.heroCard}>
|
|
|
|
|
|
<View style={styles.heroIconWrap}>
|
|
|
|
|
|
<MaterialCommunityIcons name="account-group-outline" size={28} color={colors.primary.main} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<Text variant="h3" style={styles.heroTitle}>搜索群聊</Text>
|
|
|
|
|
|
<Text variant="body" color={colors.text.secondary} style={styles.tip}>
|
|
|
|
|
|
输入群 ID 搜索后,你可以先查看群资料,再决定是否申请加入。
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
<View style={styles.formCard}>
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary} style={styles.label}>搜索群聊(群ID)</Text>
|
|
|
|
|
|
<View style={styles.searchRow}>
|
|
|
|
|
|
<TextInput
|
|
|
|
|
|
value={keyword}
|
|
|
|
|
|
onChangeText={setKeyword}
|
|
|
|
|
|
placeholder="例如:7391234567890"
|
|
|
|
|
|
placeholderTextColor={colors.text.hint}
|
|
|
|
|
|
style={styles.input}
|
|
|
|
|
|
editable={!searching && !joining}
|
|
|
|
|
|
autoCapitalize="none"
|
2026-03-16 17:47:10 +08:00
|
|
|
|
cursorColor={colors.primary.main}
|
|
|
|
|
|
selectionColor={`${colors.primary.main}40`}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
/>
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={[styles.searchBtn, (!keyword.trim() || searching || joining) && styles.submitBtnDisabled]}
|
|
|
|
|
|
onPress={handleSearch}
|
|
|
|
|
|
disabled={!keyword.trim() || searching || joining}
|
|
|
|
|
|
>
|
|
|
|
|
|
{searching ? (
|
|
|
|
|
|
<ActivityIndicator color="#fff" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<MaterialCommunityIcons name="magnify" size={20} color="#fff" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{group && (
|
|
|
|
|
|
<View style={styles.groupCard}>
|
|
|
|
|
|
<View style={styles.groupHeader}>
|
|
|
|
|
|
<Avatar source={group.avatar} size={52} name={group.name} />
|
|
|
|
|
|
<View style={styles.groupMeta}>
|
|
|
|
|
|
<Text variant="body" style={styles.groupName} numberOfLines={1}>{group.name}</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
{!!group.description && (
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary} style={styles.groupDesc}>
|
|
|
|
|
|
{group.description}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<View style={styles.groupInfoRow}>
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary}>
|
|
|
|
|
|
成员 {group.member_count}/{group.max_members}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary}>
|
|
|
|
|
|
{getJoinTypeText(group.join_type)}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<View style={styles.groupNoRow}>
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary}>
|
|
|
|
|
|
群号:{formatGroupNo(group.id)}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<TouchableOpacity style={styles.copyBtn} onPress={handleCopyGroupId}>
|
|
|
|
|
|
<MaterialCommunityIcons name="content-copy" size={14} color={colors.primary.main} />
|
|
|
|
|
|
<Text variant="caption" color={colors.primary.main} style={styles.copyBtnText}>复制</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={[styles.submitBtn, joining && styles.submitBtnDisabled]}
|
|
|
|
|
|
onPress={handleJoin}
|
|
|
|
|
|
disabled={joining}
|
|
|
|
|
|
>
|
|
|
|
|
|
{joining ? (
|
|
|
|
|
|
<ActivityIndicator color="#fff" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<MaterialCommunityIcons name="send-outline" size={18} color="#fff" />
|
|
|
|
|
|
<Text variant="body" color="#fff" style={styles.submitText}>申请入群</Text>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{searched && !group && !searching && (
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary} style={styles.emptyText}>
|
|
|
|
|
|
暂无搜索结果,请检查群ID后重试
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
|
|
|
container: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
backgroundColor: colors.background.default,
|
|
|
|
|
|
padding: spacing.lg,
|
|
|
|
|
|
},
|
|
|
|
|
|
heroCard: {
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
borderRadius: borderRadius.lg,
|
|
|
|
|
|
padding: spacing.lg,
|
|
|
|
|
|
marginBottom: spacing.lg,
|
|
|
|
|
|
},
|
|
|
|
|
|
heroIconWrap: {
|
|
|
|
|
|
width: 48,
|
|
|
|
|
|
height: 48,
|
|
|
|
|
|
borderRadius: 24,
|
|
|
|
|
|
backgroundColor: colors.primary.light + '26',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
heroTitle: {
|
|
|
|
|
|
marginBottom: spacing.xs,
|
|
|
|
|
|
},
|
|
|
|
|
|
tip: {
|
|
|
|
|
|
lineHeight: 20,
|
|
|
|
|
|
},
|
|
|
|
|
|
formCard: {
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
borderRadius: borderRadius.lg,
|
|
|
|
|
|
padding: spacing.lg,
|
|
|
|
|
|
},
|
|
|
|
|
|
label: {
|
|
|
|
|
|
marginBottom: spacing.xs,
|
|
|
|
|
|
},
|
|
|
|
|
|
input: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
borderColor: colors.divider,
|
|
|
|
|
|
borderRadius: borderRadius.md,
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
paddingHorizontal: spacing.md,
|
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
},
|
|
|
|
|
|
searchRow: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
searchBtn: {
|
|
|
|
|
|
width: 46,
|
|
|
|
|
|
height: 46,
|
|
|
|
|
|
borderRadius: borderRadius.md,
|
|
|
|
|
|
backgroundColor: colors.primary.main,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
marginLeft: spacing.sm,
|
|
|
|
|
|
},
|
|
|
|
|
|
groupCard: {
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
borderColor: colors.divider,
|
|
|
|
|
|
borderRadius: borderRadius.md,
|
|
|
|
|
|
padding: spacing.md,
|
|
|
|
|
|
backgroundColor: colors.background.default,
|
|
|
|
|
|
},
|
|
|
|
|
|
groupHeader: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
},
|
|
|
|
|
|
groupMeta: {
|
|
|
|
|
|
marginLeft: spacing.md,
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
groupName: {
|
|
|
|
|
|
marginBottom: spacing.xs,
|
|
|
|
|
|
},
|
|
|
|
|
|
groupDesc: {
|
|
|
|
|
|
marginTop: spacing.sm,
|
|
|
|
|
|
lineHeight: 18,
|
|
|
|
|
|
},
|
|
|
|
|
|
groupInfoRow: {
|
|
|
|
|
|
marginTop: spacing.sm,
|
|
|
|
|
|
marginBottom: spacing.xs,
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
},
|
|
|
|
|
|
groupNoRow: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
copyBtn: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
paddingHorizontal: spacing.sm,
|
|
|
|
|
|
paddingVertical: 4,
|
|
|
|
|
|
borderRadius: borderRadius.sm,
|
|
|
|
|
|
backgroundColor: colors.primary.light + '22',
|
|
|
|
|
|
},
|
|
|
|
|
|
copyBtnText: {
|
|
|
|
|
|
marginLeft: 4,
|
|
|
|
|
|
},
|
|
|
|
|
|
submitBtn: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
borderRadius: borderRadius.md,
|
|
|
|
|
|
backgroundColor: colors.primary.main,
|
|
|
|
|
|
minHeight: 46,
|
|
|
|
|
|
},
|
|
|
|
|
|
submitBtnDisabled: {
|
|
|
|
|
|
opacity: 0.5,
|
|
|
|
|
|
},
|
|
|
|
|
|
submitText: {
|
|
|
|
|
|
marginLeft: spacing.xs,
|
|
|
|
|
|
},
|
|
|
|
|
|
emptyText: {
|
|
|
|
|
|
marginTop: spacing.sm,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
export default JoinGroupScreen;
|