Files
frontend/src/screens/profile/FollowListScreen.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

496 lines
14 KiB
TypeScript

/**
* FollowListScreen 关注/粉丝列表页面(响应式适配)
* 显示用户的关注列表或粉丝列表
* 支持互关状态显示和关注/回关操作
* 在宽屏下使用网格布局
*/
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
FlatList,
StyleSheet,
RefreshControl,
TouchableOpacity,
ListRenderItem,
ActivityIndicator,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { User } from '../../types';
import { useAuthStore, useUserStore } from '../../stores';
import { authService } from '../../services';
import { Avatar, Text, Button, Loading, EmptyState, ResponsiveContainer } from '../../components/common';
import { HomeStackParamList } from '../../navigation/types';
import { useResponsive, useColumnCount } from '../../hooks';
type NavigationProp = NativeStackNavigationProp<HomeStackParamList, 'FollowList'>;
type FollowListRouteProp = RouteProp<HomeStackParamList, 'FollowList'>;
const FollowListScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp>();
const route = useRoute<FollowListRouteProp>();
const { userId, type } = route.params;
const { currentUser } = useAuthStore();
const { followUser, unfollowUser } = useUserStore();
// 响应式布局
const { isWideScreen, isDesktop, width } = useResponsive();
const columnCount = useColumnCount({
xs: 1,
sm: 1,
md: 2,
lg: 2,
xl: 3,
'2xl': 3,
'3xl': 4,
'4xl': 4,
});
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const isCurrentUser = currentUser?.id === userId;
const title = type === 'following' ? '关注' : '粉丝';
// 加载用户列表
const loadUsers = useCallback(async (pageNum: number = 1, refresh: boolean = false) => {
if (!hasMore && !refresh) return;
try {
const pageSize = 20;
let userList: User[] = [];
if (type === 'following') {
userList = await authService.getFollowingList(userId, pageNum, pageSize);
} else {
userList = await authService.getFollowersList(userId, pageNum, pageSize);
}
if (refresh) {
setUsers(userList);
setPage(1);
} else {
setUsers(prev => [...prev, ...userList]);
}
setHasMore(userList.length === pageSize);
} catch (error) {
console.error('加载用户列表失败:', error);
}
setLoading(false);
setRefreshing(false);
}, [userId, type, hasMore]);
// 初始加载
useEffect(() => {
loadUsers(1, true);
}, [userId, type]);
// 下拉刷新
const onRefresh = useCallback(() => {
setRefreshing(true);
setHasMore(true);
loadUsers(1, true);
}, [loadUsers]);
// 加载更多
const loadMore = useCallback(() => {
if (!loading && hasMore) {
const nextPage = page + 1;
setPage(nextPage);
loadUsers(nextPage);
}
}, [loading, hasMore, page, loadUsers]);
// 关注/取消关注
const handleFollowToggle = async (user: User) => {
const isFollowing = user.is_following ?? false;
// 乐观更新
setUsers(prev => prev.map(u => {
if (u.id === user.id) {
return {
...u,
is_following: !isFollowing,
followers_count: isFollowing ? u.followers_count - 1 : u.followers_count + 1,
};
}
return u;
}));
if (isFollowing) {
await unfollowUser(user.id);
} else {
await followUser(user.id);
}
};
// 跳转到用户主页
const handleUserPress = (targetUserId: string) => {
if (targetUserId !== currentUser?.id) {
navigation.push('UserProfile', { userId: targetUserId });
}
};
// 获取按钮状态
const getButtonConfig = (user: User): { title: string; variant: 'primary' | 'outline' } => {
const isFollowing = user.is_following ?? false;
const isFollowingMe = user.is_following_me ?? false;
if (isFollowing && isFollowingMe) {
// 已互关
return { title: '互相关注', variant: 'outline' };
} else if (isFollowing) {
// 已关注但对方未回关
return { title: '已关注', variant: 'outline' };
} else if (isFollowingMe) {
// 对方关注了我,但我没关注对方
return { title: '回关', variant: 'primary' };
} else {
// 互不关注
return { title: '关注', variant: 'primary' };
}
};
// 渲染用户项 - 列表模式(移动端)
const renderUserListItem: ListRenderItem<User> = ({ item }) => {
const buttonConfig = getButtonConfig(item);
const isSelf = item.id === currentUser?.id;
return (
<TouchableOpacity
style={styles.userItem}
onPress={() => handleUserPress(item.id)}
activeOpacity={0.7}
>
<Avatar
source={item.avatar}
size={52}
name={item.nickname}
/>
<View style={styles.userInfo}>
<Text variant="body" style={styles.nickname} numberOfLines={1}>
{item.nickname}
</Text>
<Text variant="caption" color={colors.text.secondary} numberOfLines={1}>
@{item.username}
</Text>
{item.bio && (
<Text variant="caption" color={colors.text.hint} numberOfLines={1} style={styles.bio}>
{item.bio}
</Text>
)}
</View>
{!isSelf && (
<Button
title={buttonConfig.title}
variant={buttonConfig.variant}
size="sm"
onPress={() => handleFollowToggle(item)}
style={styles.followButton}
/>
)}
</TouchableOpacity>
);
};
// 渲染用户卡片 - 网格模式(宽屏)
const renderUserGridItem: ListRenderItem<User> = ({ item }) => {
const buttonConfig = getButtonConfig(item);
const isSelf = item.id === currentUser?.id;
return (
<TouchableOpacity
style={styles.userCard}
onPress={() => handleUserPress(item.id)}
activeOpacity={0.7}
>
<View style={styles.userCardHeader}>
<Avatar
source={item.avatar}
size={56}
name={item.nickname}
/>
</View>
<View style={styles.userCardContent}>
<Text variant="body" style={styles.userCardNickname} numberOfLines={1}>
{item.nickname}
</Text>
<Text variant="caption" color={colors.text.secondary} numberOfLines={1}>
@{item.username}
</Text>
{item.bio && (
<Text variant="caption" color={colors.text.hint} numberOfLines={2} style={styles.userCardBio}>
{item.bio}
</Text>
)}
</View>
{!isSelf && (
<View style={styles.userCardFooter}>
<Button
title={buttonConfig.title}
variant={buttonConfig.variant}
size="sm"
onPress={() => handleFollowToggle(item)}
style={styles.userCardFollowButton}
/>
</View>
)}
</TouchableOpacity>
);
};
// 渲染空状态
const renderEmpty = () => {
if (loading) return <Loading />;
const emptyText = type === 'following'
? '还没有关注任何人'
: '还没有粉丝';
const emptyDesc = type === 'following'
? '去发现更多有趣的用户吧'
: '发布更多优质内容来吸引粉丝吧';
return (
<EmptyState
title={emptyText}
description={emptyDesc}
icon={type === 'following' ? 'account-plus-outline' : 'account-group-outline'}
variant="modern"
/>
);
};
const renderListHeader = () => (
<View style={styles.headerCard}>
<View style={styles.headerAccent} />
<View style={styles.headerMainRow}>
<Text variant="h2" style={styles.headerTitle}>{title}</Text>
<View style={styles.headerCountPill}>
<Text variant="caption" color={colors.text.secondary}>
{users.length}
</Text>
</View>
</View>
<Text variant="caption" color={colors.text.secondary} style={styles.headerSubtitle}>
{type === 'following' ? '你已关注的用户' : '关注你的用户'}
</Text>
</View>
);
const renderFooter = () => {
if (!hasMore || loading || users.length === 0) return null;
return (
<View style={styles.footerLoading}>
<ActivityIndicator size="small" color={colors.primary.main} />
<Text variant="caption" color={colors.text.secondary} style={styles.footerLoadingText}>
</Text>
</View>
);
};
// 宽屏使用网格布局
if (isWideScreen && columnCount > 1) {
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ResponsiveContainer maxWidth={1200}>
<FlatList
data={users}
renderItem={renderUserGridItem}
keyExtractor={item => item.id}
key={`grid-${columnCount}`}
numColumns={columnCount}
contentContainerStyle={[
styles.gridContent,
users.length === 0 && styles.emptyGridContent,
]}
columnWrapperStyle={styles.gridRow}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
onEndReached={loadMore}
onEndReachedThreshold={0.3}
ListHeaderComponent={renderListHeader}
ListEmptyComponent={renderEmpty}
ListFooterComponent={renderFooter}
showsVerticalScrollIndicator={false}
/>
</ResponsiveContainer>
</SafeAreaView>
);
}
// 移动端使用列表布局
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<FlatList
data={users}
renderItem={renderUserListItem}
keyExtractor={item => item.id}
contentContainerStyle={styles.listContent}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
onEndReached={loadMore}
onEndReachedThreshold={0.3}
ListHeaderComponent={renderListHeader}
ListEmptyComponent={renderEmpty}
ListFooterComponent={renderFooter}
showsVerticalScrollIndicator={false}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
// 列表布局样式
listContent: {
flexGrow: 1,
paddingHorizontal: spacing.md,
paddingTop: spacing.md,
paddingBottom: spacing.lg,
},
headerCard: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.xl,
paddingHorizontal: spacing.lg,
paddingVertical: spacing.md,
marginBottom: spacing.md,
borderWidth: 1,
borderColor: colors.divider + '4A',
...shadows.sm,
},
headerAccent: {
width: 28,
height: 3,
borderRadius: 999,
backgroundColor: colors.primary.main + 'A6',
marginBottom: spacing.sm,
},
headerMainRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
headerTitle: {
fontWeight: '700',
},
headerCountPill: {
paddingHorizontal: spacing.sm,
paddingVertical: 4,
borderRadius: borderRadius.full,
backgroundColor: colors.background.default,
},
headerSubtitle: {
marginTop: spacing.xs,
},
userItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.md,
marginBottom: spacing.sm,
borderWidth: 1,
borderColor: colors.divider + '45',
...shadows.sm,
},
userInfo: {
flex: 1,
marginLeft: spacing.md,
marginRight: spacing.sm,
},
nickname: {
fontWeight: '600',
marginBottom: 2,
},
bio: {
marginTop: 2,
},
followButton: {
minWidth: 82,
},
// 网格布局样式
gridContent: {
flexGrow: 1,
padding: spacing.lg,
},
emptyGridContent: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
gridRow: {
justifyContent: 'flex-start',
gap: spacing.md,
marginBottom: spacing.md,
},
userCard: {
flex: 1,
backgroundColor: colors.background.paper,
borderRadius: borderRadius.xl,
padding: spacing.md,
alignItems: 'center',
borderWidth: 1,
borderColor: colors.divider + '45',
...shadows.sm,
},
userCardHeader: {
marginBottom: spacing.sm,
},
userCardContent: {
alignItems: 'center',
width: '100%',
},
userCardNickname: {
fontWeight: '600',
marginBottom: 2,
textAlign: 'center',
},
userCardBio: {
marginTop: spacing.xs,
textAlign: 'center',
},
userCardFooter: {
marginTop: spacing.md,
width: '100%',
},
userCardFollowButton: {
width: '100%',
},
footerLoading: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
gap: spacing.xs,
},
footerLoadingText: {
marginLeft: spacing.xs,
},
});
export default FollowListScreen;