2026-04-22 16:54:51 +08:00
|
|
|
|
/**
|
2026-04-25 00:39:40 +08:00
|
|
|
|
* 统一的用户主页组件 - Twitter/X 风格
|
|
|
|
|
|
* 支持两种模式:self(当前用户)和 other(其他用户)
|
|
|
|
|
|
* 采用 Twitter/X 扁平化设计,全宽布局,无边框卡片
|
2026-03-24 04:23:13 +08:00
|
|
|
|
* 支持桌面端双栏布局
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-04-26 01:11:30 +08:00
|
|
|
|
import React, { useCallback, useMemo, useRef } from 'react';
|
2026-03-24 04:23:13 +08:00
|
|
|
|
import {
|
|
|
|
|
|
View,
|
|
|
|
|
|
RefreshControl,
|
|
|
|
|
|
ScrollView,
|
|
|
|
|
|
} from 'react-native';
|
2026-04-26 01:11:30 +08:00
|
|
|
|
import { FlashList, ListRenderItem } from '@shopify/flash-list';
|
2026-03-24 04:23:13 +08:00
|
|
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
2026-03-25 05:16:54 +08:00
|
|
|
|
import { useAppColors } from '../../theme';
|
2026-03-24 04:23:13 +08:00
|
|
|
|
import { Post } from '../../types';
|
|
|
|
|
|
import { PostCard, TabBar, UserProfileHeader } from '../../components/business';
|
|
|
|
|
|
import { Loading, EmptyState, ResponsiveContainer } from '../../components/common';
|
|
|
|
|
|
import { useResponsive } from '../../hooks';
|
2026-03-25 05:16:54 +08:00
|
|
|
|
import { useUserProfile, ProfileMode, TABS, TAB_ICONS, createSharedProfileStyles } from './useUserProfile';
|
2026-03-24 04:23:13 +08:00
|
|
|
|
|
|
|
|
|
|
interface UserProfileScreenProps {
|
|
|
|
|
|
mode: ProfileMode;
|
2026-04-14 02:12:53 +08:00
|
|
|
|
userId?: string;
|
|
|
|
|
|
hasHeader?: boolean;
|
2026-03-24 04:23:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 02:12:53 +08:00
|
|
|
|
export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, userId, hasHeader = false }) => {
|
2026-03-25 05:16:54 +08:00
|
|
|
|
const colors = useAppColors();
|
|
|
|
|
|
const sharedStyles = useMemo(() => createSharedProfileStyles(colors), [colors]);
|
2026-03-24 04:23:13 +08:00
|
|
|
|
const { isDesktop, isTablet } = useResponsive();
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-03-24 04:23:13 +08:00
|
|
|
|
const {
|
|
|
|
|
|
user,
|
|
|
|
|
|
posts,
|
|
|
|
|
|
favorites,
|
|
|
|
|
|
loading,
|
|
|
|
|
|
refreshing,
|
|
|
|
|
|
activeTab,
|
|
|
|
|
|
setActiveTab,
|
|
|
|
|
|
scrollBottomInset,
|
|
|
|
|
|
onRefresh,
|
|
|
|
|
|
handleFollow,
|
|
|
|
|
|
handlePostAction,
|
|
|
|
|
|
handleFollowingPress,
|
|
|
|
|
|
handleFollowersPress,
|
|
|
|
|
|
handleMessage,
|
2026-04-25 00:39:40 +08:00
|
|
|
|
handleBlock,
|
|
|
|
|
|
isBlocked,
|
2026-04-25 11:26:13 +08:00
|
|
|
|
isBlockedProfile,
|
2026-03-24 04:23:13 +08:00
|
|
|
|
handleSettings,
|
|
|
|
|
|
handleEditProfile,
|
|
|
|
|
|
isCurrentUser,
|
|
|
|
|
|
currentUser,
|
2026-03-24 14:21:31 +08:00
|
|
|
|
} = useUserProfile({ mode, userId, isDesktop, isTablet });
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-04-26 01:11:30 +08:00
|
|
|
|
// 当前显示的帖子列表
|
|
|
|
|
|
const currentPosts = activeTab === 0 ? posts : favorites;
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-04-26 01:11:30 +08:00
|
|
|
|
// 渲染帖子项
|
|
|
|
|
|
const renderPostItem = useCallback<ListRenderItem<Post>>(({ item, index }) => {
|
|
|
|
|
|
const isPostAuthor = currentUser?.id === item.author?.id;
|
|
|
|
|
|
const isLast = index === currentPosts.length - 1;
|
2026-03-24 04:23:13 +08:00
|
|
|
|
return (
|
2026-04-26 01:11:30 +08:00
|
|
|
|
<View style={[
|
|
|
|
|
|
sharedStyles.postWrapper,
|
|
|
|
|
|
isLast && sharedStyles.lastPost,
|
|
|
|
|
|
]}>
|
|
|
|
|
|
<PostCard
|
|
|
|
|
|
post={item}
|
|
|
|
|
|
onAction={(action) => handlePostAction(item, action)}
|
|
|
|
|
|
isPostAuthor={isPostAuthor}
|
|
|
|
|
|
/>
|
2026-03-24 04:23:13 +08:00
|
|
|
|
</View>
|
|
|
|
|
|
);
|
2026-04-26 01:11:30 +08:00
|
|
|
|
}, [currentUser?.id, handlePostAction, currentPosts.length]);
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-04-26 01:11:30 +08:00
|
|
|
|
const postKeyExtractor = useCallback((item: Post) => item.id, []);
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-04-26 01:11:30 +08:00
|
|
|
|
// 渲染空状态
|
|
|
|
|
|
const renderEmptyPosts = useCallback(() => {
|
|
|
|
|
|
if (loading) return <Loading />;
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-04-26 01:11:30 +08:00
|
|
|
|
if (isBlockedProfile && activeTab === 0 && mode === 'other') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="已将该用户拉黑"
|
|
|
|
|
|
description="你已将此用户拉黑,不再显示其帖子"
|
|
|
|
|
|
icon="account-off-outline"
|
|
|
|
|
|
variant="modern"
|
|
|
|
|
|
/>
|
2026-03-24 04:23:13 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-04-26 01:11:30 +08:00
|
|
|
|
const emptyTitle = activeTab === 0
|
|
|
|
|
|
? (mode === 'self' ? '还没有帖子' : '这个用户还没有发布任何帖子')
|
|
|
|
|
|
: (mode === 'self' ? '还没有收藏' : '这个用户还没有收藏任何帖子');
|
|
|
|
|
|
const emptyDesc = activeTab === 0
|
|
|
|
|
|
? (mode === 'self' ? '分享你的想法,发布第一条帖子吧' : '')
|
|
|
|
|
|
: (mode === 'self' ? '发现喜欢的内容,点击收藏按钮保存' : '');
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-03-24 04:23:13 +08:00
|
|
|
|
return (
|
2026-04-26 01:11:30 +08:00
|
|
|
|
<EmptyState
|
|
|
|
|
|
title={emptyTitle}
|
|
|
|
|
|
description={emptyDesc}
|
|
|
|
|
|
icon={activeTab === 0 ? 'file-document-edit-outline' : 'bookmark-heart-outline'}
|
|
|
|
|
|
variant="modern"
|
2026-03-24 04:23:13 +08:00
|
|
|
|
/>
|
|
|
|
|
|
);
|
2026-04-26 01:11:30 +08:00
|
|
|
|
}, [loading, activeTab, mode, isBlockedProfile]);
|
|
|
|
|
|
|
2026-04-26 12:04:27 +08:00
|
|
|
|
// 渲染用户信息头部(与 TabBar 分离,避免切换 tab 时重渲染)
|
|
|
|
|
|
const renderUserHeader = useMemo(() => {
|
|
|
|
|
|
if (!user) return null;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<UserProfileHeader
|
|
|
|
|
|
user={user}
|
|
|
|
|
|
isCurrentUser={isCurrentUser}
|
|
|
|
|
|
isBlocked={isBlocked}
|
|
|
|
|
|
onFollow={handleFollow}
|
|
|
|
|
|
onSettings={handleSettings}
|
|
|
|
|
|
onEditProfile={handleEditProfile}
|
|
|
|
|
|
onMessage={handleMessage}
|
|
|
|
|
|
onBlock={handleBlock}
|
|
|
|
|
|
onFollowingPress={handleFollowingPress}
|
|
|
|
|
|
onFollowersPress={handleFollowersPress}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}, [user, isCurrentUser, isBlocked, handleFollow, handleSettings, handleEditProfile, handleMessage, handleBlock, handleFollowingPress, handleFollowersPress]);
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染 TabBar
|
|
|
|
|
|
const renderTabBar = useMemo(() => (
|
|
|
|
|
|
<View style={sharedStyles.tabBarContainer}>
|
|
|
|
|
|
<TabBar
|
|
|
|
|
|
tabs={TABS}
|
|
|
|
|
|
activeIndex={activeTab}
|
|
|
|
|
|
onTabChange={setActiveTab}
|
|
|
|
|
|
variant="modern"
|
|
|
|
|
|
icons={TAB_ICONS}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
), [activeTab, setActiveTab, sharedStyles.tabBarContainer]);
|
|
|
|
|
|
|
2026-04-26 01:11:30 +08:00
|
|
|
|
// 渲染 FlashList 头部(用户信息 + TabBar)
|
|
|
|
|
|
const renderListHeader = useCallback(() => {
|
|
|
|
|
|
if (!user) return null;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
2026-04-26 12:04:27 +08:00
|
|
|
|
{renderUserHeader}
|
|
|
|
|
|
{renderTabBar}
|
2026-04-26 01:11:30 +08:00
|
|
|
|
</>
|
|
|
|
|
|
);
|
2026-04-26 12:04:27 +08:00
|
|
|
|
}, [user, renderUserHeader, renderTabBar]);
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-03-24 04:23:13 +08:00
|
|
|
|
// 未登录/用户不存在状态
|
|
|
|
|
|
if (mode === 'self' && !currentUser) {
|
|
|
|
|
|
return (
|
2026-04-14 02:12:53 +08:00
|
|
|
|
<SafeAreaView style={sharedStyles.container} edges={hasHeader ? ['bottom'] : ['top', 'bottom']}>
|
2026-03-24 04:23:13 +08:00
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="未登录"
|
|
|
|
|
|
description="请先登录"
|
|
|
|
|
|
icon="account-off-outline"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-03-24 04:23:13 +08:00
|
|
|
|
if (mode === 'other' && !user && !loading) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<SafeAreaView style={sharedStyles.container} edges={['bottom']}>
|
|
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="用户不存在"
|
|
|
|
|
|
description="该用户可能已被删除"
|
|
|
|
|
|
icon="account-off-outline"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-04-14 02:12:53 +08:00
|
|
|
|
const safeAreaEdges = hasHeader ? ['bottom'] : (mode === 'self' ? ['top', 'bottom'] : ['bottom']);
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-03-24 04:23:13 +08:00
|
|
|
|
// 桌面端使用双栏布局
|
|
|
|
|
|
if (isDesktop || isTablet) {
|
|
|
|
|
|
return (
|
2026-04-14 02:12:53 +08:00
|
|
|
|
<SafeAreaView style={sharedStyles.container} edges={safeAreaEdges as any}>
|
2026-04-25 00:39:40 +08:00
|
|
|
|
<ResponsiveContainer maxWidth={1200}>
|
2026-03-24 04:23:13 +08:00
|
|
|
|
<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}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2026-04-26 01:11:30 +08:00
|
|
|
|
{renderListHeader()}
|
2026-03-24 04:23:13 +08:00
|
|
|
|
</ScrollView>
|
|
|
|
|
|
</View>
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-03-24 04:23:13 +08:00
|
|
|
|
{/* 右侧:帖子列表 */}
|
|
|
|
|
|
<View style={sharedStyles.desktopContent}>
|
2026-04-26 01:11:30 +08:00
|
|
|
|
<FlashList
|
|
|
|
|
|
data={currentPosts}
|
|
|
|
|
|
renderItem={renderPostItem}
|
|
|
|
|
|
keyExtractor={postKeyExtractor}
|
|
|
|
|
|
ListHeaderComponent={
|
|
|
|
|
|
<View style={sharedStyles.tabBarContainer}>
|
|
|
|
|
|
<TabBar
|
|
|
|
|
|
tabs={TABS}
|
|
|
|
|
|
activeIndex={activeTab}
|
|
|
|
|
|
onTabChange={setActiveTab}
|
|
|
|
|
|
variant="modern"
|
|
|
|
|
|
icons={TAB_ICONS}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
}
|
|
|
|
|
|
ListEmptyComponent={renderEmptyPosts}
|
|
|
|
|
|
contentContainerStyle={{ paddingBottom: scrollBottomInset }}
|
2026-03-24 04:23:13 +08:00
|
|
|
|
showsVerticalScrollIndicator={false}
|
2026-04-26 01:11:30 +08:00
|
|
|
|
refreshControl={
|
|
|
|
|
|
<RefreshControl
|
|
|
|
|
|
refreshing={refreshing}
|
|
|
|
|
|
onRefresh={onRefresh}
|
|
|
|
|
|
colors={[colors.primary.main]}
|
|
|
|
|
|
tintColor={colors.primary.main}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
drawDistance={250}
|
|
|
|
|
|
/>
|
2026-03-24 04:23:13 +08:00
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</ResponsiveContainer>
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-04-25 00:39:40 +08:00
|
|
|
|
|
2026-03-24 04:23:13 +08:00
|
|
|
|
// 移动端使用单栏布局
|
|
|
|
|
|
return (
|
2026-04-14 02:12:53 +08:00
|
|
|
|
<SafeAreaView style={sharedStyles.container} edges={safeAreaEdges as any}>
|
2026-04-26 01:11:30 +08:00
|
|
|
|
<FlashList
|
|
|
|
|
|
data={currentPosts}
|
|
|
|
|
|
renderItem={renderPostItem}
|
|
|
|
|
|
keyExtractor={postKeyExtractor}
|
|
|
|
|
|
ListHeaderComponent={renderListHeader}
|
|
|
|
|
|
ListEmptyComponent={renderEmptyPosts}
|
|
|
|
|
|
contentContainerStyle={{ paddingBottom: scrollBottomInset }}
|
2026-03-24 04:23:13 +08:00
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
|
|
refreshControl={
|
|
|
|
|
|
<RefreshControl
|
|
|
|
|
|
refreshing={refreshing}
|
|
|
|
|
|
onRefresh={onRefresh}
|
|
|
|
|
|
colors={[colors.primary.main]}
|
|
|
|
|
|
tintColor={colors.primary.main}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
2026-04-26 01:11:30 +08:00
|
|
|
|
drawDistance={250}
|
|
|
|
|
|
/>
|
2026-03-24 04:23:13 +08:00
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-25 00:39:40 +08:00
|
|
|
|
export default UserProfileScreen;
|