Files
frontend/src/screens/profile/UserScreen.tsx

559 lines
17 KiB
TypeScript
Raw Normal View History

/**
* UserScreen
* BBS -
*
*/
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
FlatList,
StyleSheet,
RefreshControl,
ScrollView,
Alert,
} 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 } from '../../theme';
import { Post, User } from '../../types';
import { useUserStore } from '../../stores';
import { useCurrentUser } from '../../stores/authStore';
import { authService, postService, messageService } from '../../services';
import { userManager } from '../../stores/userManager';
import { UserProfileHeader, PostCard, TabBar } from '../../components/business';
import { Loading, EmptyState, ResponsiveContainer } from '../../components/common';
import { useResponsive } from '../../hooks';
import { HomeStackParamList, RootStackParamList } from '../../navigation/types';
type NavigationProp = NativeStackNavigationProp<HomeStackParamList, 'UserProfile'>;
type UserRouteProp = RouteProp<HomeStackParamList, 'UserProfile'>;
const TABS = ['帖子', '收藏'];
const TAB_ICONS = ['file-document-outline', 'bookmark-outline'];
export const UserScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp>();
const route = useRoute<UserRouteProp>();
const userId = route.params?.userId || '';
// 使用 any 类型来访问根导航
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { followUser, unfollowUser, likePost, unlikePost, favoritePost, unfavoritePost, posts: storePosts } = useUserStore();
const currentUser = useCurrentUser();
// 响应式布局
const { isDesktop, isTablet } = useResponsive();
const [user, setUser] = useState<User | null>(null);
const [posts, setPosts] = useState<Post[]>([]);
const [favorites, setFavorites] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [activeTab, setActiveTab] = useState(0);
const [isBlocked, setIsBlocked] = useState(false);
// 加载用户信息
const loadUserData = useCallback(async (forceRefresh = false) => {
if (!userId) {
setLoading(false);
return;
}
try {
// 强制从服务器获取最新数据,确保关注状态是最新的
const userData = await userManager.getUserById(userId, forceRefresh);
setUser(userData || null);
const blockStatus = await authService.getBlockStatus(userId);
setIsBlocked(blockStatus);
const response = await postService.getUserPosts(userId);
setPosts(response.list);
} catch (error) {
console.error('加载用户数据失败:', error);
}
setLoading(false);
}, [userId]);
// 加载用户收藏
const loadUserFavorites = useCallback(async () => {
if (!userId) return;
try {
console.log('[UserScreen] getUserFavorites called, userId:', userId);
const response = await postService.getUserFavorites(userId);
console.log('[UserScreen] getUserFavorites response:', response);
setFavorites(response.list);
} catch (error) {
console.error('获取用户收藏失败:', error);
}
}, [userId]);
// 监听 tab 切换
useEffect(() => {
if (activeTab === 1) {
loadUserFavorites();
}
}, [activeTab, loadUserFavorites]);
useEffect(() => {
// 首次加载时强制刷新,确保关注状态是最新的
loadUserData(true);
}, [loadUserData]);
// 同步 store 中的帖子状态到本地(用于点赞、收藏等状态更新)
useEffect(() => {
// 同步帖子列表状态
if (posts.length > 0) {
let hasChanges = false;
const updatedPosts = posts.map(localPost => {
const storePost = storePosts.find(sp => sp.id === localPost.id);
if (storePost && (
storePost.is_liked !== localPost.is_liked ||
storePost.is_favorited !== localPost.is_favorited ||
storePost.likes_count !== localPost.likes_count ||
storePost.favorites_count !== localPost.favorites_count
)) {
hasChanges = true;
return {
...localPost,
is_liked: storePost.is_liked,
is_favorited: storePost.is_favorited,
likes_count: storePost.likes_count,
favorites_count: storePost.favorites_count,
};
}
return localPost;
});
if (hasChanges) {
setPosts(updatedPosts);
}
}
// 同步收藏列表状态
if (favorites.length > 0) {
let hasChanges = false;
const updatedFavorites = favorites.map(localPost => {
const storePost = storePosts.find(sp => sp.id === localPost.id);
if (storePost && (
storePost.is_liked !== localPost.is_liked ||
storePost.is_favorited !== localPost.is_favorited ||
storePost.likes_count !== localPost.likes_count ||
storePost.favorites_count !== localPost.favorites_count
)) {
hasChanges = true;
return {
...localPost,
is_liked: storePost.is_liked,
is_favorited: storePost.is_favorited,
likes_count: storePost.likes_count,
favorites_count: storePost.favorites_count,
};
}
return localPost;
});
if (hasChanges) {
setFavorites(updatedFavorites);
}
}
}, [storePosts]);
// 下拉刷新
const onRefresh = useCallback(() => {
setRefreshing(true);
loadUserData(true);
setRefreshing(false);
}, [loadUserData]);
// 关注/取消关注
const handleFollow = () => {
if (!user) return;
if (user.is_following) {
unfollowUser(user.id);
setUser({ ...user, is_following: false, followers_count: user.followers_count - 1 });
} else {
followUser(user.id);
setUser({ ...user, is_following: true, followers_count: user.followers_count + 1 });
}
};
// 跳转到关注列表
const handleFollowingPress = () => {
(rootNavigation as any).navigate('FollowList', { userId, type: 'following' });
};
// 跳转到粉丝列表
const handleFollowersPress = () => {
(rootNavigation as any).navigate('FollowList', { userId, type: 'followers' });
};
// 跳转到帖子详情
const handlePostPress = (postId: string, scrollToComments: boolean = false) => {
navigation.navigate('PostDetail', { postId, scrollToComments });
};
// 跳转到用户主页(这里不做处理)
const handleUserPress = (postUserId: string) => {
if (postUserId !== userId) {
navigation.push('UserProfile', { userId: postUserId });
}
};
// 删除帖子
const handleDeletePost = async (postId: string) => {
try {
const success = await postService.deletePost(postId);
if (success) {
// 从帖子列表中移除
setPosts(prev => prev.filter(p => p.id !== postId));
// 也从收藏列表中移除(如果存在)
setFavorites(prev => prev.filter(p => p.id !== postId));
} else {
console.error('删除帖子失败');
}
} catch (error) {
console.error('删除帖子失败:', error);
throw error;
}
};
// 跳转到聊天界面
const handleMessage = async () => {
if (!user) return;
try {
// 前端只提供对方的用户ID会话ID由后端生成
const conversation = await messageService.createConversation(user.id);
if (conversation) {
// 跳转到聊天界面 - 使用 rootNavigation 确保在正确的导航栈中
(rootNavigation as any).navigate('Chat', {
conversationId: conversation.id.toString(),
userId: user.id
});
}
} catch (error) {
console.error('创建会话失败:', error);
}
};
// 处理更多按钮点击
const handleMore = () => {
if (!user) return;
Alert.alert(
'更多操作',
undefined,
[
{ text: '取消', style: 'cancel' },
{
text: isBlocked ? '取消拉黑' : '拉黑用户',
style: 'destructive',
onPress: () => {
Alert.alert(
isBlocked ? '确认取消拉黑' : '确认拉黑',
isBlocked
? '取消拉黑后,对方可以重新与你建立关系。'
: '拉黑后,对方将无法给你发送私聊消息,且会互相移除关注关系。',
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
style: 'destructive',
onPress: async () => {
const ok = isBlocked
? await authService.unblockUser(user.id)
: await authService.blockUser(user.id);
if (!ok) {
Alert.alert('失败', isBlocked ? '取消拉黑失败,请稍后重试' : '拉黑失败,请稍后重试');
return;
}
setUser(prev =>
prev
? {
...prev,
is_following: false,
is_following_me: false,
}
: prev
);
setIsBlocked(!isBlocked);
Alert.alert('成功', isBlocked ? '已取消拉黑' : '已拉黑该用户');
},
},
]
);
},
},
]
);
};
// 渲染内容
const renderContent = () => {
if (loading) return <Loading />;
if (activeTab === 0) {
// 帖子
if (posts.length === 0) {
return (
<EmptyState
title="还没有帖子"
description="这个用户还没有发布任何帖子"
icon="file-document-edit-outline"
variant="modern"
/>
);
}
return (
<View style={styles.postsContainer}>
{posts.map((post, index) => {
const isPostAuthor = currentUser?.id === post.author?.id;
return (
<View key={post.id} style={[
styles.postWrapper,
index === posts.length - 1 && styles.lastPost,
]}>
<PostCard
post={post}
onPress={() => handlePostPress(post.id)}
onUserPress={() => post.author ? handleUserPress(post.author.id) : () => {}}
onLike={() => post.is_liked ? unlikePost(post.id) : likePost(post.id)}
onComment={() => handlePostPress(post.id, true)}
onBookmark={() => post.is_favorited ? unfavoritePost(post.id) : favoritePost(post.id)}
onShare={() => {}}
onDelete={() => handleDeletePost(post.id)}
isPostAuthor={isPostAuthor}
/>
</View>
);
})}
</View>
);
}
if (activeTab === 1) {
// 收藏
if (favorites.length === 0) {
return (
<EmptyState
title="还没有收藏"
description="这个用户还没有收藏任何帖子"
icon="bookmark-heart-outline"
variant="modern"
/>
);
}
return (
<View style={styles.postsContainer}>
{favorites.map((post, index) => {
const isPostAuthor = currentUser?.id === post.author?.id;
return (
<View key={post.id} style={[
styles.postWrapper,
index === favorites.length - 1 && styles.lastPost,
]}>
<PostCard
post={post}
onPress={() => handlePostPress(post.id)}
onUserPress={() => post.author ? handleUserPress(post.author.id) : () => {}}
onLike={() => post.is_liked ? unlikePost(post.id) : likePost(post.id)}
onComment={() => handlePostPress(post.id, true)}
onBookmark={() => post.is_favorited ? unfavoritePost(post.id) : favoritePost(post.id)}
onShare={() => {}}
onDelete={() => handleDeletePost(post.id)}
isPostAuthor={isPostAuthor}
/>
</View>
);
})}
</View>
);
}
return null;
};
// 渲染用户信息头部
const renderUserHeader = () => {
if (!user) return null;
return (
<UserProfileHeader
user={user}
isCurrentUser={false}
onFollow={handleFollow}
onMessage={handleMessage}
onMore={handleMore}
onFollowingPress={handleFollowingPress}
onFollowersPress={handleFollowersPress}
/>
);
};
// 渲染 TabBar 和内容
const renderTabBarAndContent = () => (
<>
<View style={styles.tabBarContainer}>
<TabBar
tabs={TABS}
activeIndex={activeTab}
onTabChange={setActiveTab}
variant="modern"
icons={TAB_ICONS}
/>
</View>
<View style={styles.contentContainer}>
{renderContent()}
</View>
</>
);
if (loading) {
return <Loading fullScreen />;
}
if (!user) {
return (
<SafeAreaView style={styles.container}>
<EmptyState
title="用户不存在"
description="该用户可能已被删除"
icon="account-off-outline"
/>
</SafeAreaView>
);
}
// 桌面端使用双栏布局
if (isDesktop || isTablet) {
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ResponsiveContainer maxWidth={1400}>
<View style={styles.desktopContainer}>
{/* 左侧:用户信息 */}
<View style={styles.desktopSidebar}>
<ScrollView
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
>
{renderUserHeader()}
</ScrollView>
</View>
{/* 右侧:帖子列表 */}
<View style={styles.desktopContent}>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.desktopScrollContent}
>
{renderTabBarAndContent()}
</ScrollView>
</View>
</View>
</ResponsiveContainer>
</SafeAreaView>
);
}
// 移动端使用单栏布局
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<FlatList
data={[{ key: 'header' }]}
renderItem={({ item }) => (
<View>
{renderUserHeader()}
<View style={styles.tabBarContainer}>
<TabBar
tabs={TABS}
activeIndex={activeTab}
onTabChange={setActiveTab}
variant="modern"
icons={TAB_ICONS}
/>
</View>
<View style={styles.contentContainer}>
{renderContent()}
</View>
</View>
)}
keyExtractor={item => item.key}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
// 桌面端双栏布局
desktopContainer: {
flex: 1,
flexDirection: 'row',
gap: spacing.lg,
padding: spacing.lg,
},
desktopSidebar: {
width: 380,
flexShrink: 0,
},
desktopContent: {
flex: 1,
minWidth: 0,
},
desktopScrollContent: {
flexGrow: 1,
},
tabBarContainer: {
marginTop: spacing.sm,
marginBottom: spacing.xs,
},
contentContainer: {
flex: 1,
minHeight: 350,
paddingTop: spacing.sm,
},
postsContainer: {
paddingHorizontal: spacing.md,
paddingTop: spacing.sm,
},
postWrapper: {
marginBottom: spacing.md,
backgroundColor: colors.background.paper,
borderRadius: 16,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 8,
elevation: 2,
},
lastPost: {
marginBottom: spacing['2xl'],
},
});
export default UserScreen;