Files
frontend/src/screens/profile/ProfileScreen.tsx
lafay 1ffbb63753
Some checks failed
Frontend CI / ota-android (push) Successful in 10m56s
Frontend CI / build-android-apk (push) Failing after 33m45s
Frontend CI / build-and-push-web (push) Failing after 46s
fix: 统一修复所有屏幕的安全区域问题
- 有 header 的屏幕使用 edges={['bottom']}(React Navigation 自动处理顶部)
- 没有 header 的屏幕使用 edges={['top', 'bottom']}(需要自己处理顶部)
- 修复了以下屏幕:
  - HomeScreen: edges={['bottom']} → ['top', 'bottom']
  - MessageListScreen: edges={['top']} → ['top', 'bottom']
  - ScheduleScreen: edges={['top']} → ['top', 'bottom']
  - ProfileScreen: edges={['top', 'bottom']} ✓ 已正确
  - PostDetailScreen: 空状态添加 edges={['bottom']}
  - UserScreen: 空状态添加 edges={['bottom']}
  - NotificationsScreen: edges={[]} → ['bottom']
  - SearchScreen: edges={['left', 'right']} → ['bottom']
  - BlockedUsersScreen: 加载状态添加 edges={['bottom']}
  - GroupInfoScreen: edges={['bottom']} ✓ 已正确
  - PrivateChatInfoScreen: 加载状态添加 edges={['bottom']}
2026-03-18 10:58:41 +08:00

496 lines
16 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.
/**
* 个人主页 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();
// 在大屏幕模式下不使用 Bottom Tab Navigator所以 try-catch 处理
let tabBarHeight = 0;
try {
tabBarHeight = useBottomTabBarHeight();
} catch (e) {
// 大屏幕模式下不在 Bottom Tab Navigator 中,会抛出错误,忽略即可
tabBarHeight = 0;
}
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 {
const response = await postService.getUserFavorites(currentUser.id);
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} edges={['top', 'bottom']}>
<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;