refactor(PostCard, PostCard.legacy): remove legacy PostCard component and update exports
- Deleted the legacy PostCard component to streamline the codebase and improve maintainability. - Updated exports in PostCard and index files to remove references to the legacy component. - Adjusted PostCardProps to eliminate legacy properties, ensuring only the new API is supported. - Enhanced the PostCard component to focus on modern implementation and features.
This commit is contained in:
@@ -512,14 +512,7 @@ export const HomeScreen: React.FC = () => {
|
||||
<PostCard
|
||||
post={post}
|
||||
variant="grid"
|
||||
onPress={() => handlePostPress(post.id)}
|
||||
onUserPress={() => authorId && handleUserPress(authorId)}
|
||||
onLike={() => handleLike(post)}
|
||||
onComment={() => handlePostPress(post.id, true)}
|
||||
onBookmark={() => handleBookmark(post)}
|
||||
onShare={() => handleShare(post)}
|
||||
onImagePress={(images, index) => handleImagePress(images, index)}
|
||||
onDelete={() => handleDeletePost(post.id)}
|
||||
onAction={(action) => handlePostAction(post, action)}
|
||||
isPostAuthor={isPostAuthor}
|
||||
/>
|
||||
</View>
|
||||
@@ -579,15 +572,8 @@ export const HomeScreen: React.FC = () => {
|
||||
<PostCard
|
||||
key={post.id}
|
||||
post={post}
|
||||
variant={viewMode === 'grid' ? 'grid' : 'default'}
|
||||
onPress={() => handlePostPress(post.id)}
|
||||
onUserPress={() => authorId && handleUserPress(authorId)}
|
||||
onLike={() => handleLike(post)}
|
||||
onComment={() => handlePostPress(post.id, true)}
|
||||
onBookmark={() => handleBookmark(post)}
|
||||
onShare={() => handleShare(post)}
|
||||
onImagePress={(images, index) => handleImagePress(images, index)}
|
||||
onDelete={() => handleDeletePost(post.id)}
|
||||
variant={viewMode === 'grid' ? 'grid' : 'list'}
|
||||
onAction={(action) => handlePostAction(post, action)}
|
||||
isPostAuthor={isPostAuthor}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -281,8 +281,25 @@ export const PostDetailScreen: React.FC = () => {
|
||||
if (!post?.author) return;
|
||||
|
||||
const author = post.author;
|
||||
const handleBackPress = () => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack();
|
||||
return;
|
||||
}
|
||||
navigation.navigate('Main', {
|
||||
screen: 'HomeTab',
|
||||
params: { screen: 'Home', params: undefined },
|
||||
});
|
||||
};
|
||||
|
||||
navigation.setOptions({
|
||||
headerBackVisible: false,
|
||||
headerTitleAlign: 'left',
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity onPress={handleBackPress} style={styles.headerBackButton} hitSlop={8}>
|
||||
<MaterialCommunityIcons name="arrow-left" size={24} color={colors.text.primary} />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
headerTitle: () => (
|
||||
<View style={styles.headerContainer}>
|
||||
<TouchableOpacity
|
||||
@@ -298,7 +315,9 @@ export const PostDetailScreen: React.FC = () => {
|
||||
<View style={styles.headerUserInfo}>
|
||||
<TouchableOpacity onPress={() => handleUserPress(author.id)}>
|
||||
<Text style={styles.headerNickname} numberOfLines={1}>
|
||||
{author.nickname}
|
||||
{author.nickname && author.nickname.length > 10
|
||||
? `${author.nickname.slice(0, 10)}...`
|
||||
: author.nickname}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -1701,7 +1720,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
marginLeft: -8,
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
headerAvatarWrapper: {
|
||||
marginRight: spacing.sm,
|
||||
@@ -1720,6 +1739,12 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
headerBackButton: {
|
||||
paddingHorizontal: spacing.xs,
|
||||
paddingVertical: spacing.xs,
|
||||
marginLeft: spacing.xs,
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
// 帖子容器
|
||||
postContainer: {
|
||||
backgroundColor: colors.background.paper,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Post, User } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
import { postService, authService } from '../../services';
|
||||
import { PostCard, TabBar, SearchBar } from '../../components/business';
|
||||
import type { PostCardAction } from '../../components/business/PostCard';
|
||||
import { Avatar, EmptyState, Text, ResponsiveGrid, Loading } from '../../components/common';
|
||||
import { HomeStackParamList } from '../../navigation/types';
|
||||
import { useResponsive, useResponsiveSpacing, useResponsiveValue } from '../../hooks/useResponsive';
|
||||
@@ -175,6 +176,41 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
navigation.navigate('UserProfile', { userId });
|
||||
};
|
||||
|
||||
// 统一处理 PostCard 的操作(搜索页不支持删除)
|
||||
const handlePostAction = (post: Post, action: PostCardAction) => {
|
||||
switch (action.type) {
|
||||
case 'press':
|
||||
handlePostPress(post.id);
|
||||
break;
|
||||
case 'userPress':
|
||||
if (post.author?.id) {
|
||||
handleUserPress(post.author.id);
|
||||
}
|
||||
break;
|
||||
case 'like':
|
||||
postService.likePost(post.id);
|
||||
break;
|
||||
case 'unlike':
|
||||
postService.unlikePost(post.id);
|
||||
break;
|
||||
case 'comment':
|
||||
handlePostPress(post.id, true);
|
||||
break;
|
||||
case 'bookmark':
|
||||
postService.favoritePost(post.id);
|
||||
break;
|
||||
case 'unbookmark':
|
||||
postService.unfavoritePost(post.id);
|
||||
break;
|
||||
case 'share':
|
||||
// 搜索页暂不处理分享
|
||||
break;
|
||||
case 'imagePress':
|
||||
case 'delete':
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前搜索类型
|
||||
const getSearchType = (): SearchType => {
|
||||
switch (activeIndex) {
|
||||
@@ -222,13 +258,9 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
<PostCard
|
||||
key={post.id}
|
||||
post={post}
|
||||
onPress={() => handlePostPress(post.id)}
|
||||
onUserPress={() => post.author ? handleUserPress(post.author.id) : () => {}}
|
||||
onLike={() => {}}
|
||||
onComment={() => handlePostPress(post.id, true)}
|
||||
onBookmark={() => {}}
|
||||
onShare={() => {}}
|
||||
compact={isMobile}
|
||||
onAction={(action) => handlePostAction(post, action)}
|
||||
variant="list"
|
||||
features={isMobile ? 'compact' : 'full'}
|
||||
/>
|
||||
))}
|
||||
</ResponsiveGrid>
|
||||
@@ -248,13 +280,9 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack, navigation:
|
||||
renderItem={({ item }) => (
|
||||
<PostCard
|
||||
post={item}
|
||||
onPress={() => handlePostPress(item.id)}
|
||||
onUserPress={() => item.author ? handleUserPress(item.author.id) : () => {}}
|
||||
onLike={() => {}}
|
||||
onComment={() => handlePostPress(item.id, true)}
|
||||
onBookmark={() => {}}
|
||||
onShare={() => {}}
|
||||
compact
|
||||
onAction={(action) => handlePostAction(item, action)}
|
||||
variant="list"
|
||||
features="compact"
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => item.id}
|
||||
|
||||
@@ -78,6 +78,8 @@ export const ChatScreen: React.FC = () => {
|
||||
// 输入框区域高度(用于定位浮动 mention 面板)
|
||||
const [inputWrapperHeight, setInputWrapperHeight] = useState(60);
|
||||
const [showEdgeLoadingIndicator, setShowEdgeLoadingIndicator] = useState(false);
|
||||
const replyTargetMessageIdRef = useRef<string | null>(null);
|
||||
const replyHighlightTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const isPreloadingRef = useRef(false);
|
||||
const lastScrollYRef = useRef(0);
|
||||
const isUserDraggingRef = useRef(false);
|
||||
@@ -151,6 +153,7 @@ export const ChatScreen: React.FC = () => {
|
||||
longPressMenuVisible,
|
||||
selectedMessage,
|
||||
selectedMessageId,
|
||||
setSelectedMessageId,
|
||||
menuPosition,
|
||||
isGroupChat,
|
||||
groupInfo,
|
||||
@@ -206,6 +209,7 @@ export const ChatScreen: React.FC = () => {
|
||||
handleReachLatestEdge,
|
||||
setBrowsingHistory,
|
||||
} = useChatScreen();
|
||||
const displayMessages = useMemo(() => [...messages].reverse(), [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loadingMore && showEdgeLoadingIndicator) {
|
||||
@@ -218,14 +222,46 @@ export const ChatScreen: React.FC = () => {
|
||||
if (preloadCooldownTimerRef.current) {
|
||||
clearTimeout(preloadCooldownTimerRef.current);
|
||||
}
|
||||
if (replyHighlightTimerRef.current) {
|
||||
clearTimeout(replyHighlightTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const highlightMessageTemporarily = useCallback((messageId: string, duration = 1500) => {
|
||||
if (replyHighlightTimerRef.current) {
|
||||
clearTimeout(replyHighlightTimerRef.current);
|
||||
}
|
||||
setSelectedMessageId(messageId);
|
||||
replyHighlightTimerRef.current = setTimeout(() => {
|
||||
setSelectedMessageId(prev => (prev === messageId ? null : prev));
|
||||
}, duration);
|
||||
}, [setSelectedMessageId]);
|
||||
|
||||
const handleReplyPreviewPress = useCallback((messageId: string) => {
|
||||
const targetId = String(messageId);
|
||||
const targetIndex = displayMessages.findIndex(msg => String(msg.id) === targetId);
|
||||
if (targetIndex < 0) return;
|
||||
|
||||
replyTargetMessageIdRef.current = targetId;
|
||||
setBrowsingHistory(true);
|
||||
highlightMessageTemporarily(targetId);
|
||||
|
||||
flatListRef.current?.scrollToIndex({
|
||||
index: targetIndex,
|
||||
animated: true,
|
||||
viewPosition: 0.5,
|
||||
});
|
||||
}, [displayMessages, flatListRef, highlightMessageTemporarily, setBrowsingHistory]);
|
||||
|
||||
// 监听返回事件,刷新会话列表
|
||||
useEffect(() => {
|
||||
const unsubscribe = navigation.addListener('beforeRemove', () => {
|
||||
// 刷新会话列表,确保已读状态正确显示
|
||||
messageManager.fetchConversations(true);
|
||||
messageManager.requestConversationListRefresh('chat-before-remove', {
|
||||
force: true,
|
||||
allowDefer: false,
|
||||
});
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [navigation]);
|
||||
@@ -261,6 +297,7 @@ export const ChatScreen: React.FC = () => {
|
||||
shouldShowTime={shouldShowTime}
|
||||
onImagePress={handleImagePress}
|
||||
onReply={handleReplyMessage}
|
||||
onReplyPress={handleReplyPreviewPress}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
@@ -280,14 +317,13 @@ export const ChatScreen: React.FC = () => {
|
||||
shouldShowTime,
|
||||
handleImagePress,
|
||||
handleReplyMessage,
|
||||
handleReplyPreviewPress,
|
||||
]);
|
||||
|
||||
const keyExtractor = useCallback((item: any) => String(item.id), []);
|
||||
|
||||
// 获取正在输入提示
|
||||
const typingHint = getTypingHint();
|
||||
const displayMessages = useMemo(() => [...messages].reverse(), [messages]);
|
||||
|
||||
const handleMessageListScroll = useCallback((event: any) => {
|
||||
const { contentSize, contentOffset, layoutMeasurement } = event.nativeEvent;
|
||||
scrollPositionRef.current = {
|
||||
@@ -410,6 +446,26 @@ export const ChatScreen: React.FC = () => {
|
||||
}}
|
||||
scrollEventThrottle={16}
|
||||
onContentSizeChange={handleContentSizeChange}
|
||||
onScrollToIndexFailed={(info) => {
|
||||
const targetId = replyTargetMessageIdRef.current;
|
||||
if (!targetId || !flatListRef.current) return;
|
||||
|
||||
flatListRef.current.scrollToOffset({
|
||||
offset: Math.max(0, info.averageItemLength * info.index),
|
||||
animated: true,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
const retryIndex = displayMessages.findIndex(msg => String(msg.id) === targetId);
|
||||
if (retryIndex >= 0) {
|
||||
flatListRef.current?.scrollToIndex({
|
||||
index: retryIndex,
|
||||
animated: true,
|
||||
viewPosition: 0.5,
|
||||
});
|
||||
}
|
||||
}, 120);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showEdgeLoadingIndicator && (
|
||||
|
||||
@@ -57,6 +57,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
shouldShowTime,
|
||||
onImagePress,
|
||||
onReply,
|
||||
onReplyPress,
|
||||
}) => {
|
||||
const bubbleRef = useRef<View>(null);
|
||||
const pressPositionRef = useRef({ x: 0, y: 0 });
|
||||
@@ -306,6 +307,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
isMe ? styles.myBubble : styles.theirBubble,
|
||||
hasReply && segmentStyles.replyBubble,
|
||||
isPureImageMessage && segmentStyles.pureImageBubble,
|
||||
isSelected && (isMe ? styles.mySelectedBubble : styles.theirSelectedBubble),
|
||||
]}>
|
||||
<MessageSegmentsRenderer
|
||||
segments={segments}
|
||||
@@ -315,7 +317,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
replyMessage={getReplyMessage()}
|
||||
getSenderInfo={getSenderInfo}
|
||||
onAtPress={() => undefined}
|
||||
onReplyPress={() => undefined}
|
||||
onReplyPress={onReplyPress}
|
||||
onImagePress={(url) => {
|
||||
// 查找点击的图片索引
|
||||
const clickIndex = imageSegments.findIndex(img => img.url === url);
|
||||
|
||||
@@ -86,6 +86,8 @@ export interface MessageBubbleProps {
|
||||
onImagePress?: (images: { id: string; url: string; thumbnail_url?: string; width?: number; height?: number }[], index: number) => void;
|
||||
// 滑动回复回调
|
||||
onReply?: (message: GroupMessage) => void;
|
||||
// 点击回复预览回调(定位到原消息)
|
||||
onReplyPress?: (messageId: string) => void;
|
||||
}
|
||||
|
||||
// 输入框 Props
|
||||
|
||||
@@ -124,6 +124,8 @@ export const useChatScreen = () => {
|
||||
const isProgrammaticScrollRef = useRef(false);
|
||||
const suppressAutoFollowRef = useRef(false);
|
||||
const isBrowsingHistoryRef = useRef(false);
|
||||
const hasShownMessageListRef = useRef(false);
|
||||
const historyLoadingLockUntilRef = useRef(0);
|
||||
|
||||
// 回复消息状态
|
||||
const [replyingTo, setReplyingTo] = useState<GroupMessage | null>(null);
|
||||
@@ -205,9 +207,16 @@ export const useChatScreen = () => {
|
||||
// 加载态语义修正:
|
||||
// isLoadingMessages 在分页加载历史时也会短暂为 true。
|
||||
// 若直接驱动 ChatScreen 的 loading,会导致消息列表被卸载重挂载,触发“回到底部”。
|
||||
// 这里只在“首屏且尚无消息”时展示 loading 占位。
|
||||
// 这里只在“首屏且尚未展示过消息列表”时展示 loading 占位。
|
||||
useEffect(() => {
|
||||
setLoading(isLoadingMessages && messageManagerMessages.length === 0);
|
||||
if (messageManagerMessages.length > 0) {
|
||||
hasShownMessageListRef.current = true;
|
||||
}
|
||||
const shouldShowInitialLoading =
|
||||
!hasShownMessageListRef.current &&
|
||||
isLoadingMessages &&
|
||||
messageManagerMessages.length === 0;
|
||||
setLoading(shouldShowInitialLoading);
|
||||
}, [isLoadingMessages, messageManagerMessages.length]);
|
||||
|
||||
// 【改造】同步 hasMore 状态
|
||||
@@ -281,10 +290,16 @@ export const useChatScreen = () => {
|
||||
enterMarkedKeyRef.current = '';
|
||||
suppressAutoFollowRef.current = false;
|
||||
isBrowsingHistoryRef.current = false;
|
||||
hasShownMessageListRef.current = false;
|
||||
historyLoadingLockUntilRef.current = 0;
|
||||
}, [conversationId]);
|
||||
|
||||
const isHistoryLoadingLocked = useCallback(() => {
|
||||
return Date.now() < historyLoadingLockUntilRef.current;
|
||||
}, []);
|
||||
|
||||
const scrollToLatest = useCallback((animated: boolean, force: boolean = false, reason: string = 'unknown') => {
|
||||
if (!force && (suppressAutoFollowRef.current || isBrowsingHistoryRef.current)) return;
|
||||
if (!force && (suppressAutoFollowRef.current || isBrowsingHistoryRef.current || isHistoryLoadingLocked())) return;
|
||||
// inverted 列表下,最新消息端对应 offset=0
|
||||
isProgrammaticScrollRef.current = true;
|
||||
flatListRef.current?.scrollToOffset({
|
||||
@@ -294,7 +309,7 @@ export const useChatScreen = () => {
|
||||
setTimeout(() => {
|
||||
isProgrammaticScrollRef.current = false;
|
||||
}, animated ? 220 : 32);
|
||||
}, [flatListRef, scrollPositionRef, messages.length]);
|
||||
}, [flatListRef, isHistoryLoadingLocked]);
|
||||
|
||||
const isNearBottom = useCallback(() => {
|
||||
const scrollY = scrollPositionRef.current.scrollY || 0;
|
||||
@@ -443,6 +458,8 @@ export const useChatScreen = () => {
|
||||
|
||||
// 历史加载期间禁止“新消息自动跟随到底”
|
||||
suppressAutoFollowRef.current = true;
|
||||
isBrowsingHistoryRef.current = true;
|
||||
historyLoadingLockUntilRef.current = Date.now() + 3000;
|
||||
|
||||
// 保存加载前的滚动位置和内容高度
|
||||
const scrollYBefore = scrollPositionRef.current.scrollY;
|
||||
@@ -464,6 +481,8 @@ export const useChatScreen = () => {
|
||||
console.error('加载历史消息失败:', error);
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
// 加载结束后继续保留短暂锁窗,避免布局结算阶段误触自动回底
|
||||
historyLoadingLockUntilRef.current = Date.now() + 800;
|
||||
}
|
||||
}, [conversationId, hasMoreHistory, loadingMore, loadMoreMessages, messages]);
|
||||
|
||||
@@ -473,9 +492,10 @@ export const useChatScreen = () => {
|
||||
}, []);
|
||||
|
||||
const handleReachLatestEdge = useCallback(() => {
|
||||
if (isHistoryLoadingLocked()) return;
|
||||
suppressAutoFollowRef.current = false;
|
||||
isBrowsingHistoryRef.current = false;
|
||||
}, []);
|
||||
}, [isHistoryLoadingLocked]);
|
||||
|
||||
const setBrowsingHistory = useCallback((browsing: boolean) => {
|
||||
isBrowsingHistoryRef.current = browsing;
|
||||
@@ -1221,6 +1241,7 @@ export const useChatScreen = () => {
|
||||
longPressMenuVisible,
|
||||
selectedMessage,
|
||||
selectedMessageId,
|
||||
setSelectedMessageId,
|
||||
menuPosition,
|
||||
isGroupChat,
|
||||
groupInfo,
|
||||
|
||||
@@ -1,523 +1,21 @@
|
||||
/**
|
||||
* 个人主页 ProfileScreen - 美化版(响应式适配)
|
||||
* 个人主页 ProfileScreen
|
||||
* 胡萝卜BBS - 当前用户个人主页
|
||||
* 采用现代卡片式设计,优化视觉层次和交互体验
|
||||
* 支持桌面端双栏布局
|
||||
* 使用统一的 UserProfileScreen 组件,mode='self'
|
||||
*/
|
||||
|
||||
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 React from 'react';
|
||||
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 { PostCardAction } from '../../components/business/PostCard';
|
||||
import { Loading, EmptyState, Text } from '../../components/common';
|
||||
import { ResponsiveContainer } from '../../components/common';
|
||||
import { useResponsive } from '../../hooks';
|
||||
import { ProfileStackParamList, HomeStackParamList, RootStackParamList } from '../../navigation/types';
|
||||
import UserProfileScreen from './UserProfileScreen';
|
||||
import { ProfileStackParamList } 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'];
|
||||
type ProfileNavigationProp = NativeStackNavigationProp<ProfileStackParamList, 'Profile'>;
|
||||
|
||||
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, posts: storePosts } = useUserStore();
|
||||
const profileNavigation = useNavigation<ProfileNavigationProp>();
|
||||
|
||||
// 响应式布局
|
||||
const { isDesktop, isTablet, width } = useResponsive();
|
||||
|
||||
// 页面滚动底部安全间距,避免内容被底部 TabBar 遮挡
|
||||
// TabBar 悬浮:高度 64 + 浮动间距 12*2 = 88
|
||||
const scrollBottomInset = useMemo(() => {
|
||||
if (isDesktop) return spacing.lg;
|
||||
const FLOATING_TAB_BAR_HEIGHT = 64 + 24; // TabBar高度 + 上下浮动间距
|
||||
return FLOATING_TAB_BAR_HEIGHT + insets.bottom + spacing.md;
|
||||
}, [isDesktop, 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;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 统一的 PostCard Action 处理
|
||||
const handlePostAction = useCallback((post: Post, action: PostCardAction) => {
|
||||
switch (action.type) {
|
||||
case 'press':
|
||||
handlePostPress(post.id);
|
||||
break;
|
||||
case 'userPress':
|
||||
if (post.author) {
|
||||
handleUserPress(post.author.id);
|
||||
}
|
||||
break;
|
||||
case 'like':
|
||||
case 'unlike':
|
||||
post.is_liked ? postService.unlikePost(post.id) : postService.likePost(post.id);
|
||||
break;
|
||||
case 'comment':
|
||||
handlePostPress(post.id, true);
|
||||
break;
|
||||
case 'bookmark':
|
||||
case 'unbookmark':
|
||||
post.is_favorited ? postService.unfavoritePost(post.id) : postService.favoritePost(post.id);
|
||||
break;
|
||||
case 'share':
|
||||
// 暂不处理分享
|
||||
break;
|
||||
case 'delete':
|
||||
handleDeletePost(post.id);
|
||||
break;
|
||||
}
|
||||
}, [handlePostPress, handleUserPress, handleDeletePost]);
|
||||
|
||||
// 渲染内容
|
||||
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}
|
||||
onAction={(action) => handlePostAction(post, action)}
|
||||
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 ? postService.unlikePost(post.id) : postService.likePost(post.id)}
|
||||
onComment={() => handlePostPress(post.id, true)}
|
||||
onBookmark={() => post.is_favorited ? postService.unfavoritePost(post.id) : postService.favoritePost(post.id)}
|
||||
onShare={() => {}}
|
||||
onDelete={() => handleDeletePost(post.id)}
|
||||
isPostAuthor={isPostAuthor}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [loading, activeTab, posts, favorites, currentUser?.id, handlePostPress, handleUserPress, handleDeletePost]);
|
||||
|
||||
// 渲染用户信息头部 - 不随 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>
|
||||
);
|
||||
return <UserProfileScreen mode="self" profileNavigation={profileNavigation} />;
|
||||
};
|
||||
|
||||
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;
|
||||
export default ProfileScreen;
|
||||
235
src/screens/profile/UserProfileScreen.tsx
Normal file
235
src/screens/profile/UserProfileScreen.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* 统一的用户主页组件
|
||||
* 胡萝卜BBS - 支持两种模式:self(当前用户)和 other(其他用户)
|
||||
* 采用现代卡片式设计,优化视觉层次和交互体验
|
||||
* 支持桌面端双栏布局
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { colors } from '../../theme';
|
||||
import { Post } from '../../types';
|
||||
import { PostCard, TabBar, UserProfileHeader } from '../../components/business';
|
||||
import { Loading, EmptyState, ResponsiveContainer } from '../../components/common';
|
||||
import { useResponsive } from '../../hooks';
|
||||
import { ProfileStackParamList } from '../../navigation/types';
|
||||
import { useUserProfile, ProfileMode, TABS, TAB_ICONS, sharedStyles } from './useUserProfile';
|
||||
|
||||
interface UserProfileScreenProps {
|
||||
mode: ProfileMode;
|
||||
userId?: string; // 仅 other 模式需要
|
||||
profileNavigation?: NativeStackNavigationProp<ProfileStackParamList>; // 仅 self 模式需要
|
||||
}
|
||||
|
||||
export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, userId, profileNavigation }) => {
|
||||
const { isDesktop, isTablet } = useResponsive();
|
||||
|
||||
const {
|
||||
user,
|
||||
posts,
|
||||
favorites,
|
||||
loading,
|
||||
refreshing,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
scrollBottomInset,
|
||||
onRefresh,
|
||||
handleFollow,
|
||||
handlePostAction,
|
||||
handleFollowingPress,
|
||||
handleFollowersPress,
|
||||
handleMessage,
|
||||
handleMore,
|
||||
handleSettings,
|
||||
handleEditProfile,
|
||||
isCurrentUser,
|
||||
currentUser,
|
||||
} = useUserProfile({ mode, userId, isDesktop, isTablet, profileNavigation });
|
||||
|
||||
// 渲染帖子列表
|
||||
const renderPostList = useCallback((postList: Post[], emptyTitle: string, emptyDesc: string) => {
|
||||
if (postList.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
title={emptyTitle}
|
||||
description={emptyDesc}
|
||||
icon={activeTab === 0 ? 'file-document-edit-outline' : 'bookmark-heart-outline'}
|
||||
variant="modern"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={sharedStyles.postsContainer}>
|
||||
{postList.map((post, index) => {
|
||||
const isPostAuthor = currentUser?.id === post.author?.id;
|
||||
return (
|
||||
<View key={post.id} style={[
|
||||
sharedStyles.postWrapper,
|
||||
index === postList.length - 1 && sharedStyles.lastPost,
|
||||
]}>
|
||||
<PostCard
|
||||
post={post}
|
||||
onAction={(action) => handlePostAction(post, action)}
|
||||
isPostAuthor={isPostAuthor}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}, [currentUser?.id, handlePostAction, activeTab]);
|
||||
|
||||
// 渲染内容
|
||||
const renderContent = useCallback(() => {
|
||||
if (loading) return <Loading />;
|
||||
|
||||
if (activeTab === 0) {
|
||||
return renderPostList(
|
||||
posts,
|
||||
mode === 'self' ? '还没有帖子' : '这个用户还没有发布任何帖子',
|
||||
mode === 'self' ? '分享你的想法,发布第一条帖子吧' : ''
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === 1) {
|
||||
return renderPostList(
|
||||
favorites,
|
||||
mode === 'self' ? '还没有收藏' : '这个用户还没有收藏任何帖子',
|
||||
mode === 'self' ? '发现喜欢的内容,点击收藏按钮保存' : ''
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [loading, activeTab, posts, favorites, mode, renderPostList]);
|
||||
|
||||
// 渲染用户信息头部
|
||||
const renderUserHeader = useMemo(() => {
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<UserProfileHeader
|
||||
user={user}
|
||||
isCurrentUser={isCurrentUser}
|
||||
onFollow={handleFollow}
|
||||
onSettings={handleSettings}
|
||||
onEditProfile={handleEditProfile}
|
||||
onMessage={handleMessage}
|
||||
onMore={handleMore}
|
||||
onFollowingPress={handleFollowingPress}
|
||||
onFollowersPress={handleFollowersPress}
|
||||
/>
|
||||
);
|
||||
}, [user, isCurrentUser, handleFollow, handleSettings, handleEditProfile, handleMessage, handleMore, handleFollowingPress, handleFollowersPress]);
|
||||
|
||||
// 渲染 TabBar 和内容
|
||||
const renderTabBarAndContent = useMemo(() => (
|
||||
<>
|
||||
<View style={sharedStyles.tabBarContainer}>
|
||||
<TabBar
|
||||
tabs={TABS}
|
||||
activeIndex={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
variant="modern"
|
||||
icons={TAB_ICONS}
|
||||
/>
|
||||
</View>
|
||||
<View style={sharedStyles.contentContainer}>
|
||||
{renderContent()}
|
||||
</View>
|
||||
</>
|
||||
), [activeTab, renderContent]);
|
||||
|
||||
// 未登录/用户不存在状态
|
||||
if (mode === 'self' && !currentUser) {
|
||||
return (
|
||||
<SafeAreaView style={sharedStyles.container} edges={['top', 'bottom']}>
|
||||
<EmptyState
|
||||
title="未登录"
|
||||
description="请先登录"
|
||||
icon="account-off-outline"
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === 'other' && !user && !loading) {
|
||||
return (
|
||||
<SafeAreaView style={sharedStyles.container} edges={['bottom']}>
|
||||
<EmptyState
|
||||
title="用户不存在"
|
||||
description="该用户可能已被删除"
|
||||
icon="account-off-outline"
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
// 桌面端使用双栏布局
|
||||
if (isDesktop || isTablet) {
|
||||
return (
|
||||
<SafeAreaView style={sharedStyles.container} edges={mode === 'self' ? ['top', 'bottom'] : ['bottom']}>
|
||||
<ResponsiveContainer maxWidth={1400}>
|
||||
<View style={sharedStyles.desktopContainer}>
|
||||
{/* 左侧:用户信息 */}
|
||||
<View style={sharedStyles.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={sharedStyles.desktopContent}>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={[sharedStyles.desktopScrollContent, { paddingBottom: scrollBottomInset }]}
|
||||
>
|
||||
{renderTabBarAndContent}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</ResponsiveContainer>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
// 移动端使用单栏布局
|
||||
return (
|
||||
<SafeAreaView style={sharedStyles.container} edges={mode === 'self' ? ['top', 'bottom'] : ['bottom']}>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={[colors.primary.main]}
|
||||
tintColor={colors.primary.main}
|
||||
/>
|
||||
}
|
||||
contentContainerStyle={[sharedStyles.scrollContent, { paddingBottom: scrollBottomInset }]}
|
||||
>
|
||||
{renderUserHeader}
|
||||
{renderTabBarAndContent}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileScreen;
|
||||
@@ -1,578 +1,29 @@
|
||||
/**
|
||||
* 用户主页 UserScreen(响应式适配)
|
||||
* 用户主页 UserScreen
|
||||
* 胡萝卜BBS - 查看其他用户资料
|
||||
* 支持桌面端双栏布局
|
||||
* 使用统一的 UserProfileScreen 组件,mode='other'
|
||||
*/
|
||||
|
||||
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 React from 'react';
|
||||
import { useRoute, RouteProp } from '@react-navigation/native';
|
||||
import UserProfileScreen from './UserProfileScreen';
|
||||
import { HomeStackParamList } from '../../navigation/types';
|
||||
import { useCurrentUser } from '../../stores/authStore';
|
||||
import { authService, postService, messageService } from '../../services';
|
||||
import { userManager } from '../../stores/userManager';
|
||||
import { UserProfileHeader, PostCard, TabBar } from '../../components/business';
|
||||
import { PostCardAction } from '../../components/business/PostCard';
|
||||
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, posts: storePosts } = useUserStore();
|
||||
const currentUser = useCurrentUser();
|
||||
const isSelfProfile = !!currentUser?.id && currentUser.id === userId;
|
||||
|
||||
// 响应式布局
|
||||
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 {
|
||||
const response = await postService.getUserFavorites(userId);
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// 统一处理 PostCard 的所有操作
|
||||
const handlePostAction = (post: Post, action: PostCardAction) => {
|
||||
switch (action.type) {
|
||||
case 'press':
|
||||
handlePostPress(post.id);
|
||||
break;
|
||||
case 'userPress':
|
||||
if (post.author) handleUserPress(post.author.id);
|
||||
break;
|
||||
case 'like':
|
||||
postService.likePost(post.id);
|
||||
break;
|
||||
case 'unlike':
|
||||
postService.unlikePost(post.id);
|
||||
break;
|
||||
case 'comment':
|
||||
handlePostPress(post.id, true);
|
||||
break;
|
||||
case 'bookmark':
|
||||
postService.favoritePost(post.id);
|
||||
break;
|
||||
case 'unbookmark':
|
||||
postService.unfavoritePost(post.id);
|
||||
break;
|
||||
case 'share':
|
||||
// 暂不处理
|
||||
break;
|
||||
case 'delete':
|
||||
handleDeletePost(post.id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到聊天界面
|
||||
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}
|
||||
onAction={(action) => handlePostAction(post, action)}
|
||||
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}
|
||||
onAction={(action) => handlePostAction(post, action)}
|
||||
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} edges={['bottom']}>
|
||||
<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>
|
||||
<UserProfileScreen
|
||||
mode={isSelfProfile ? 'self' : 'other'}
|
||||
userId={isSelfProfile ? undefined : userId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
* 个人中心模块导出
|
||||
*/
|
||||
|
||||
// 统一的用户主页组件
|
||||
export { UserProfileScreen } from './UserProfileScreen';
|
||||
|
||||
// 兼容旧组件(内部使用 UserProfileScreen)
|
||||
export { ProfileScreen } from './ProfileScreen';
|
||||
export { SettingsScreen } from './SettingsScreen';
|
||||
export { EditProfileScreen } from './EditProfileScreen';
|
||||
@@ -10,3 +14,7 @@ export { default as FollowListScreen } from './FollowListScreen';
|
||||
export { NotificationSettingsScreen } from './NotificationSettingsScreen';
|
||||
export { BlockedUsersScreen } from './BlockedUsersScreen';
|
||||
export { AccountSecurityScreen } from './AccountSecurityScreen';
|
||||
|
||||
// 导出 Hook 供需要自定义的场景使用
|
||||
export { useUserProfile, sharedStyles, TABS, TAB_ICONS } from './useUserProfile';
|
||||
export type { ProfileMode, UseUserProfileOptions, UseUserProfileReturn } from './useUserProfile';
|
||||
|
||||
504
src/screens/profile/useUserProfile.ts
Normal file
504
src/screens/profile/useUserProfile.ts
Normal file
@@ -0,0 +1,504 @@
|
||||
/**
|
||||
* 用户主页共享逻辑 Hook
|
||||
* 提取 ProfileScreen 和 UserScreen 的共同逻辑
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { colors, spacing } from '../../theme';
|
||||
import { Post, User } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
import { useCurrentUser } from '../../stores/authStore';
|
||||
import { postService, authService, messageService } from '../../services';
|
||||
import { userManager } from '../../stores/userManager';
|
||||
import { HomeStackParamList, RootStackParamList, ProfileStackParamList } from '../../navigation/types';
|
||||
import { PostCardAction } from '../../components/business/PostCard';
|
||||
|
||||
export type ProfileMode = 'self' | 'other';
|
||||
|
||||
export interface UseUserProfileOptions {
|
||||
mode: ProfileMode;
|
||||
userId?: string; // 仅 other 模式需要
|
||||
isDesktop?: boolean;
|
||||
isTablet?: boolean;
|
||||
profileNavigation?: NativeStackNavigationProp<ProfileStackParamList>; // 仅 self 模式需要
|
||||
}
|
||||
|
||||
export interface UseUserProfileReturn {
|
||||
// 用户数据
|
||||
user: User | null;
|
||||
posts: Post[];
|
||||
favorites: Post[];
|
||||
loading: boolean;
|
||||
refreshing: boolean;
|
||||
isBlocked: boolean;
|
||||
|
||||
// Tab 相关
|
||||
activeTab: number;
|
||||
setActiveTab: (tab: number) => void;
|
||||
|
||||
// 布局相关
|
||||
scrollBottomInset: number;
|
||||
|
||||
// 操作
|
||||
onRefresh: () => Promise<void>;
|
||||
handleFollow: () => void;
|
||||
handlePostPress: (postId: string, scrollToComments?: boolean) => void;
|
||||
handleUserPress: (postUserId: string) => void;
|
||||
handleDeletePost: (postId: string) => Promise<void>;
|
||||
handleFollowingPress: () => void;
|
||||
handleFollowersPress: () => void;
|
||||
handlePostAction: (post: Post, action: PostCardAction) => void;
|
||||
|
||||
// 仅 other 模式
|
||||
handleMessage?: () => Promise<void>;
|
||||
handleMore?: () => void;
|
||||
|
||||
// 仅 self 模式
|
||||
handleSettings?: () => void;
|
||||
handleEditProfile?: () => void;
|
||||
|
||||
// 判断
|
||||
isCurrentUser: boolean;
|
||||
currentUser: User | null;
|
||||
}
|
||||
|
||||
export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileReturn => {
|
||||
const { mode, userId, isDesktop = false, profileNavigation } = options;
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
const homeNavigation = useNavigation<NativeStackNavigationProp<HomeStackParamList>>();
|
||||
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
|
||||
const { followUser, unfollowUser, posts: storePosts } = useUserStore();
|
||||
const currentUser = useCurrentUser();
|
||||
|
||||
// 状态
|
||||
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);
|
||||
|
||||
// 获取其他用户数据(other 模式)
|
||||
const loadOtherUserData = useCallback(async (forceRefresh = false) => {
|
||||
if (mode !== 'other' || !userId) {
|
||||
setLoading(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return userData;
|
||||
} catch (error) {
|
||||
console.error('加载用户数据失败:', error);
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [mode, userId]);
|
||||
|
||||
// 加载当前用户的帖子(self 模式)
|
||||
const loadSelfPosts = useCallback(async () => {
|
||||
if (mode !== 'self' || !currentUser) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await postService.getUserPosts(currentUser.id);
|
||||
setPosts(response.list);
|
||||
} catch (error) {
|
||||
console.error('获取用户帖子失败:', error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [mode, currentUser]);
|
||||
|
||||
// 加载收藏
|
||||
const loadFavorites = useCallback(async () => {
|
||||
const targetUserId = mode === 'self' ? currentUser?.id : userId;
|
||||
if (!targetUserId) return;
|
||||
|
||||
try {
|
||||
const response = await postService.getUserFavorites(targetUserId);
|
||||
setFavorites(response.list);
|
||||
} catch (error) {
|
||||
console.error('获取用户收藏失败:', error);
|
||||
}
|
||||
}, [mode, currentUser?.id, userId]);
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
if (mode === 'self') {
|
||||
loadSelfPosts();
|
||||
} else {
|
||||
loadOtherUserData(true);
|
||||
}
|
||||
}, [mode, loadSelfPosts, loadOtherUserData]);
|
||||
|
||||
// Tab 切换加载收藏
|
||||
useEffect(() => {
|
||||
if (activeTab === 1 && favorites.length === 0) {
|
||||
loadFavorites();
|
||||
}
|
||||
}, [activeTab, favorites.length, loadFavorites]);
|
||||
|
||||
// 同步 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(async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
if (mode === 'self') {
|
||||
const { useAuthStore } = await import('../../stores');
|
||||
const { fetchCurrentUser } = useAuthStore.getState();
|
||||
await fetchCurrentUser();
|
||||
await loadSelfPosts();
|
||||
} else {
|
||||
await loadOtherUserData(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新失败:', error);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [mode, loadSelfPosts, loadOtherUserData]);
|
||||
|
||||
// 关注/取消关注
|
||||
const handleFollow = useCallback(() => {
|
||||
const targetUser = mode === 'self' ? currentUser : user;
|
||||
if (!targetUser) return;
|
||||
|
||||
if (targetUser.is_following) {
|
||||
unfollowUser(targetUser.id);
|
||||
if (mode === 'other') {
|
||||
setUser({ ...targetUser, is_following: false, followers_count: targetUser.followers_count - 1 });
|
||||
}
|
||||
} else {
|
||||
followUser(targetUser.id);
|
||||
if (mode === 'other') {
|
||||
setUser({ ...targetUser, is_following: true, followers_count: targetUser.followers_count + 1 });
|
||||
}
|
||||
}
|
||||
}, [mode, currentUser, user, unfollowUser, followUser]);
|
||||
|
||||
// 跳转到关注列表
|
||||
const handleFollowingPress = useCallback(() => {
|
||||
const targetUserId = mode === 'self' ? currentUser?.id : userId;
|
||||
if (targetUserId) {
|
||||
(rootNavigation as any).navigate('FollowList', { userId: targetUserId, type: 'following' });
|
||||
}
|
||||
}, [mode, currentUser?.id, userId, rootNavigation]);
|
||||
|
||||
// 跳转到粉丝列表
|
||||
const handleFollowersPress = useCallback(() => {
|
||||
const targetUserId = mode === 'self' ? currentUser?.id : userId;
|
||||
if (targetUserId) {
|
||||
(rootNavigation as any).navigate('FollowList', { userId: targetUserId, type: 'followers' });
|
||||
}
|
||||
}, [mode, currentUser?.id, userId, rootNavigation]);
|
||||
|
||||
// 跳转到帖子详情
|
||||
const handlePostPress = useCallback((postId: string, scrollToComments: boolean = false) => {
|
||||
homeNavigation.navigate('PostDetail', { postId, scrollToComments });
|
||||
}, [homeNavigation]);
|
||||
|
||||
// 跳转到用户主页
|
||||
const handleUserPress = useCallback((postUserId: string) => {
|
||||
if (mode === 'self') {
|
||||
return;
|
||||
}
|
||||
if (postUserId !== userId) {
|
||||
homeNavigation.push('UserProfile', { userId: postUserId });
|
||||
}
|
||||
}, [mode, userId, homeNavigation]);
|
||||
|
||||
// 删除帖子
|
||||
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;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 统一的 PostCard Action 处理
|
||||
const handlePostAction = useCallback((post: Post, action: PostCardAction) => {
|
||||
switch (action.type) {
|
||||
case 'press':
|
||||
handlePostPress(post.id);
|
||||
break;
|
||||
case 'userPress':
|
||||
if (post.author) {
|
||||
handleUserPress(post.author.id);
|
||||
}
|
||||
break;
|
||||
case 'like':
|
||||
case 'unlike':
|
||||
post.is_liked ? postService.unlikePost(post.id) : postService.likePost(post.id);
|
||||
break;
|
||||
case 'comment':
|
||||
handlePostPress(post.id, true);
|
||||
break;
|
||||
case 'bookmark':
|
||||
case 'unbookmark':
|
||||
post.is_favorited ? postService.unfavoritePost(post.id) : postService.favoritePost(post.id);
|
||||
break;
|
||||
case 'share':
|
||||
break;
|
||||
case 'delete':
|
||||
handleDeletePost(post.id);
|
||||
break;
|
||||
}
|
||||
}, [handlePostPress, handleUserPress, handleDeletePost]);
|
||||
|
||||
// 发消息(仅 other 模式)
|
||||
const handleMessage = useCallback(async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const conversation = await messageService.createConversation(user.id);
|
||||
if (conversation) {
|
||||
(rootNavigation as any).navigate('Chat', {
|
||||
conversationId: conversation.id.toString(),
|
||||
userId: user.id
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建会话失败:', error);
|
||||
}
|
||||
}, [user, rootNavigation]);
|
||||
|
||||
// 更多操作(仅 other 模式)
|
||||
const handleMore = useCallback(() => {
|
||||
if (!user) return;
|
||||
|
||||
const { Alert } = require('react-native');
|
||||
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 ? '已取消拉黑' : '已拉黑该用户');
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
}, [user, isBlocked]);
|
||||
|
||||
// 设置页跳转(仅 self 模式)
|
||||
const handleSettings = useCallback(() => {
|
||||
if (profileNavigation) {
|
||||
profileNavigation.navigate('Settings');
|
||||
}
|
||||
}, [profileNavigation]);
|
||||
|
||||
// 编辑资料页跳转(仅 self 模式)
|
||||
const handleEditProfile = useCallback(() => {
|
||||
if (profileNavigation) {
|
||||
profileNavigation.navigate('EditProfile');
|
||||
}
|
||||
}, [profileNavigation]);
|
||||
|
||||
// 计算滚动底部间距
|
||||
const scrollBottomInset = useMemo(() => {
|
||||
if (isDesktop) return spacing.lg;
|
||||
const FLOATING_TAB_BAR_HEIGHT = 64 + 24;
|
||||
return FLOATING_TAB_BAR_HEIGHT + insets.bottom + spacing.md;
|
||||
}, [isDesktop, insets.bottom]);
|
||||
|
||||
// 获取显示的用户
|
||||
const displayUser = mode === 'self' ? currentUser : user;
|
||||
|
||||
return {
|
||||
user: displayUser,
|
||||
posts,
|
||||
favorites,
|
||||
loading,
|
||||
refreshing,
|
||||
isBlocked,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
scrollBottomInset,
|
||||
onRefresh,
|
||||
handleFollow,
|
||||
handlePostPress,
|
||||
handleUserPress,
|
||||
handleDeletePost,
|
||||
handleFollowingPress,
|
||||
handleFollowersPress,
|
||||
handlePostAction,
|
||||
handleMessage: mode === 'other' ? handleMessage : undefined,
|
||||
handleMore: mode === 'other' ? handleMore : undefined,
|
||||
handleSettings: mode === 'self' && profileNavigation ? handleSettings : undefined,
|
||||
handleEditProfile: mode === 'self' && profileNavigation ? handleEditProfile : undefined,
|
||||
isCurrentUser: mode === 'self',
|
||||
currentUser,
|
||||
};
|
||||
};
|
||||
|
||||
// 共享的样式
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const sharedStyles = 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'],
|
||||
},
|
||||
});
|
||||
|
||||
// Tab 配置
|
||||
export const TABS = ['帖子', '收藏'];
|
||||
export const TAB_ICONS = ['file-document-outline', 'bookmark-outline'];
|
||||
Reference in New Issue
Block a user