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

491 lines
16 KiB
TypeScript

/**
* 个人主页 ProfileScreen - 美化版(响应式适配)
* 胡萝卜BBS - 当前用户个人主页
* 采用现代卡片式设计,优化视觉层次和交互体验
* 支持桌面端双栏布局
*/
import React, { useState, useCallback, useMemo } from 'react';
import {
View,
StyleSheet,
RefreshControl,
Animated,
ScrollView,
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { colors, spacing } from '../../theme';
import { Post } from '../../types';
import { useAuthStore, useUserStore } from '../../stores';
import { postService } from '../../services';
import { UserProfileHeader, PostCard, TabBar } from '../../components/business';
import { Loading, EmptyState, Text } from '../../components/common';
import { ResponsiveContainer } from '../../components/common';
import { useResponsive } from '../../hooks';
import { ProfileStackParamList, HomeStackParamList, RootStackParamList } from '../../navigation/types';
type NavigationProp = NativeStackNavigationProp<ProfileStackParamList, 'Profile'>;
type HomeNavigationProp = NativeStackNavigationProp<HomeStackParamList>;
type RootNavigationProp = NativeStackNavigationProp<RootStackParamList>;
const TABS = ['帖子', '收藏'];
const TAB_ICONS = ['file-document-outline', 'bookmark-outline'];
export const ProfileScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp>();
const insets = useSafeAreaInsets();
const tabBarHeight = useBottomTabBarHeight();
const homeNavigation = useNavigation<HomeNavigationProp>();
// 使用 any 类型来访问根导航
const rootNavigation = useNavigation<RootNavigationProp>();
const { currentUser, updateUser, fetchCurrentUser } = useAuthStore();
const { followUser, unfollowUser, likePost, unlikePost, favoritePost, unfavoritePost, posts: storePosts } = useUserStore();
// 响应式布局
const { isDesktop, isTablet, width } = useResponsive();
// 页面滚动底部安全间距,避免内容被底部 TabBar 遮挡
const scrollBottomInset = useMemo(() => {
if (isDesktop) return spacing.lg;
return tabBarHeight + insets.bottom + spacing.md;
}, [isDesktop, tabBarHeight, insets.bottom]);
const [activeTab, setActiveTab] = useState(0);
const [posts, setPosts] = useState<Post[]>([]);
const [favorites, setFavorites] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const scrollY = new Animated.Value(0);
// 跳转到关注列表
const handleFollowingPress = useCallback(() => {
if (currentUser) {
(rootNavigation as any).navigate('FollowList', { userId: currentUser.id, type: 'following' });
}
}, [currentUser, rootNavigation]);
// 跳转到粉丝列表
const handleFollowersPress = useCallback(() => {
if (currentUser) {
(rootNavigation as any).navigate('FollowList', { userId: currentUser.id, type: 'followers' });
}
}, [currentUser, rootNavigation]);
// 加载用户帖子
const loadUserPosts = useCallback(async () => {
if (currentUser) {
try {
const response = await postService.getUserPosts(currentUser.id);
setPosts(response.list);
} catch (error) {
console.error('获取用户帖子失败:', error);
}
}
setLoading(false);
}, [currentUser]);
// 加载用户收藏
const loadUserFavorites = useCallback(async () => {
if (currentUser) {
try {
console.log('[ProfileScreen] load, userUserFavorites calledId:', currentUser.id);
const response = await postService.getUserFavorites(currentUser.id);
console.log('[ProfileScreen] getUserFavorites response:', response);
setFavorites(response.list);
} catch (error) {
console.error('获取用户收藏失败:', error);
}
}
setLoading(false);
}, [currentUser]);
// 监听 tab 切换,只在数据为空时加载对应数据
React.useEffect(() => {
if (activeTab === 0 && posts.length === 0) {
loadUserPosts();
} else if (activeTab === 1 && favorites.length === 0) {
loadUserFavorites();
}
}, [activeTab, loadUserPosts, loadUserFavorites, posts.length, favorites.length]);
// 初始加载
React.useEffect(() => {
loadUserPosts();
}, [loadUserPosts]);
// 同步 store 中的帖子状态到本地(用于点赞、收藏等状态更新)
React.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(async () => {
setRefreshing(true);
try {
// 刷新用户信息
await fetchCurrentUser();
// 刷新帖子列表
await loadUserPosts();
} catch (error) {
console.error('刷新失败:', error);
} finally {
setRefreshing(false);
}
}, [fetchCurrentUser, loadUserPosts]);
// 跳转到设置页
const handleSettings = useCallback(() => {
navigation.navigate('Settings');
}, [navigation]);
// 跳转到编辑资料页
const handleEditProfile = useCallback(() => {
navigation.navigate('EditProfile');
}, [navigation]);
// 关注/取消关注
const handleFollow = useCallback(() => {
if (!currentUser) return;
if (currentUser.is_following) {
unfollowUser(currentUser.id);
} else {
followUser(currentUser.id);
}
}, [currentUser, unfollowUser, followUser]);
// 跳转到帖子详情
const handlePostPress = useCallback((postId: string, scrollToComments: boolean = false) => {
homeNavigation.navigate('PostDetail', { postId, scrollToComments });
}, [homeNavigation]);
// 跳转到用户主页(当前用户)
const handleUserPress = useCallback((userId: string) => {
// 个人主页点击自己的头像不跳转
}, []);
// 删除帖子
const handleDeletePost = useCallback(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 renderContent = useCallback(() => {
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;
}, [loading, activeTab, posts, favorites, currentUser?.id, handlePostPress, handleUserPress, handleDeletePost, unlikePost, likePost, unfavoritePost, favoritePost]);
// 渲染用户信息头部 - 不随 tab 变化,使用 useMemo 缓存
const renderUserHeader = useMemo(() => (
<UserProfileHeader
user={currentUser!}
isCurrentUser={true}
onFollow={handleFollow}
onSettings={handleSettings}
onEditProfile={handleEditProfile}
onFollowingPress={handleFollowingPress}
onFollowersPress={handleFollowersPress}
/>
), [currentUser, handleFollow, handleSettings, handleEditProfile, handleFollowingPress, handleFollowersPress]);
// 渲染 TabBar 和内容
const renderTabBarAndContent = useMemo(() => (
<>
<View style={styles.tabBarContainer}>
<TabBar
tabs={TABS}
activeIndex={activeTab}
onTabChange={setActiveTab}
variant="modern"
icons={TAB_ICONS}
/>
</View>
<View style={styles.contentContainer}>
{renderContent()}
</View>
</>
), [activeTab, renderContent]);
if (!currentUser) {
return (
<SafeAreaView style={styles.container}>
<EmptyState
title="未登录"
description="请先登录"
icon="account-off-outline"
/>
</SafeAreaView>
);
}
// 桌面端使用双栏布局
if (isDesktop || isTablet) {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<ResponsiveContainer maxWidth={1400}>
<View style={styles.desktopContainer}>
{/* 左侧:用户信息 */}
<View style={styles.desktopSidebar}>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: scrollBottomInset }}
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, { paddingBottom: scrollBottomInset }]}
>
{renderTabBarAndContent}
</ScrollView>
</View>
</View>
</ResponsiveContainer>
</SafeAreaView>
);
}
// 移动端使用单栏布局
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<ScrollView
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset }]}
>
{/* 用户信息头部 - 固定在顶部,不受 tab 切换影响 */}
{renderUserHeader}
{/* TabBar - 分离出来,切换 tab 不会影响上面的用户信息 */}
{renderTabBarAndContent}
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollContent: {
flexGrow: 1,
},
// 桌面端双栏布局
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.xs,
marginBottom: 2,
},
contentContainer: {
flex: 1,
minHeight: 350,
paddingTop: spacing.xs,
},
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 ProfileScreen;