Initial frontend repository commit.

Include app source and update .gitignore to exclude local release artifacts and signing files.

Made-with: Cursor
This commit is contained in:
2026-03-09 21:29:03 +08:00
commit 3968660048
129 changed files with 55599 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
import React, { useCallback, useEffect, useState } from 'react';
import {
ActivityIndicator,
FlatList,
RefreshControl,
StyleSheet,
TouchableOpacity,
View,
Alert,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Avatar, Button, EmptyState, ResponsiveContainer, Text } from '../../components/common';
import { authService } from '../../services';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { User } from '../../types';
import { RootStackParamList } from '../../navigation/types';
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
export const BlockedUsersScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp>();
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [processingUserId, setProcessingUserId] = useState<string | null>(null);
const loadBlockedUsers = useCallback(async () => {
try {
const response = await authService.getBlockedUsers(1, 100);
setUsers(response.list || []);
} catch (error) {
console.error('加载黑名单失败:', error);
} finally {
setLoading(false);
setRefreshing(false);
}
}, []);
useEffect(() => {
loadBlockedUsers();
}, [loadBlockedUsers]);
const onRefresh = useCallback(() => {
setRefreshing(true);
loadBlockedUsers();
}, [loadBlockedUsers]);
const handleUnblock = useCallback((user: User) => {
Alert.alert(
'取消拉黑',
`确定取消拉黑 ${user.nickname} 吗?`,
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
onPress: async () => {
try {
setProcessingUserId(user.id);
const ok = await authService.unblockUser(user.id);
if (!ok) {
Alert.alert('失败', '取消拉黑失败,请稍后重试');
return;
}
setUsers(prev => prev.filter(u => u.id !== user.id));
} finally {
setProcessingUserId(null);
}
},
},
]
);
}, []);
const renderItem = ({ item }: { item: User }) => (
<TouchableOpacity
style={styles.item}
activeOpacity={0.75}
onPress={() => navigation.navigate('UserProfile', { userId: item.id })}
>
<Avatar source={item.avatar} size={46} name={item.nickname} />
<View style={styles.content}>
<Text variant="body" style={styles.nickname} numberOfLines={1}>
{item.nickname}
</Text>
<Text variant="caption" color={colors.text.secondary} numberOfLines={1}>
@{item.username}
</Text>
</View>
<Button
title={processingUserId === item.id ? '处理中...' : '取消拉黑'}
size="sm"
variant="outline"
onPress={() => handleUnblock(item)}
disabled={processingUserId === item.id}
/>
</TouchableOpacity>
);
if (loading) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.loadingWrap}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ResponsiveContainer maxWidth={900}>
<FlatList
data={users}
keyExtractor={item => item.id}
renderItem={renderItem}
contentContainerStyle={[styles.listContent, users.length === 0 && styles.emptyContent]}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
ListEmptyComponent={
<EmptyState
title="黑名单为空"
description="你还没有拉黑任何用户"
icon="account-off-outline"
variant="modern"
/>
}
/>
</ResponsiveContainer>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
loadingWrap: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
listContent: {
padding: spacing.md,
gap: spacing.sm,
},
emptyContent: {
flexGrow: 1,
justifyContent: 'center',
},
item: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.md,
...shadows.sm,
},
content: {
flex: 1,
marginLeft: spacing.md,
marginRight: spacing.sm,
},
nickname: {
fontWeight: '600',
},
});
export default BlockedUsersScreen;