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

559 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 用户主页 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;