Include app source and update .gitignore to exclude local release artifacts and signing files. Made-with: Cursor
559 lines
17 KiB
TypeScript
559 lines
17 KiB
TypeScript
/**
|
||
* 用户主页 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;
|