refactor(ui): implement custom header system and unify screen layouts
Some checks failed
Frontend CI / ota-android (push) Successful in 1m30s
Frontend CI / build-and-push-web (push) Successful in 12m29s
Frontend CI / build-android-apk (push) Failing after 3h10m38s

Refactor the navigation and header strategy by replacing native stack headers with a custom `SimpleHeader` component across most profile and detail screens. This provides a more consistent UI/UX and better control over layout behavior.

- Implement `SimpleHeader` component in `src/components/common`.
- Disable native header rendering in `app/(app)/(tabs)/profile/_layout.tsx` and `app/_layout.tsx`.
- Update profile sub-screens to use `SimpleHeader` and `StatusBar` from `expo-status-bar`.
- Refactor `PostDetailScreen` to use a custom header implementation for better integration with the post author information.
- Update `UserScreen` to wrap content in `SafeAreaView` with the new header.
- Adjust `AppBackButton` to support transparent backgrounds.
This commit is contained in:
2026-05-03 22:01:43 +08:00
parent 490a99ab3c
commit 4213d13b8f
21 changed files with 522 additions and 173 deletions

View File

@@ -23,8 +23,9 @@ import {
Dimensions,
} from 'react-native';
import { FlashList, ListRenderItem, FlashListRef } from '@shopify/flash-list';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useNavigation, useRouter, useLocalSearchParams } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import {
@@ -131,6 +132,7 @@ export const PostDetailScreen: React.FC = () => {
postId?: string;
scrollToComments?: string;
}>();
const postId = postIdParam || '';
const shouldScrollToComments = scrollParam === '1' || scrollParam === 'true';
@@ -367,58 +369,43 @@ export const PostDetailScreen: React.FC = () => {
}
}, [shouldScrollToComments, isPostInitialLoading, comments?.length]);
// 动态设置导航头部
useEffect(() => {
if (!post?.author) return;
const author = post.author;
const handleBackPress = () => {
if (router.canGoBack()) {
router.back();
return;
}
router.replace(hrefs.hrefHome());
};
navigation.setOptions({
// 与页面容器 background.default 一致,并去掉原生 header 底部分隔阴影,避免「一条线」
headerStyle: { backgroundColor: colors.background.default },
headerShadowVisible: false,
headerBackVisible: false,
headerTitleAlign: 'left',
headerLeft: () => (
<AppBackButton onPress={handleBackPress} style={styles.headerBackButton} />
),
headerTitle: () => (
const handleBackPress = useCallback(() => {
if (router.canGoBack()) {
router.back();
return;
}
router.replace(hrefs.hrefHome());
}, [router]);
const renderCustomHeader = () => (
<View style={styles.customHeader}>
<AppBackButton onPress={handleBackPress} style={styles.headerBackButton} />
{post?.author ? (
<View style={styles.headerContainer}>
<TouchableOpacity
onPress={() => handleUserPress(author.id)}
onPress={() => handleUserPress(post.author!.id)}
style={styles.headerAvatarWrapper}
>
<Avatar
source={author.avatar}
size={32}
name={author.nickname}
/>
<Avatar source={post.author.avatar} size={32} name={post.author.nickname} />
</TouchableOpacity>
<View style={styles.headerUserInfo}>
<TouchableOpacity onPress={() => handleUserPress(author.id)}>
<TouchableOpacity onPress={() => handleUserPress(post.author!.id)}>
<Text style={styles.headerNickname} numberOfLines={1}>
{author.nickname && author.nickname.length > 10
? `${author.nickname.slice(0, 10)}...`
: author.nickname}
{post.author.nickname && post.author.nickname.length > 10
? `${post.author.nickname.slice(0, 10)}...`
: post.author.nickname}
</Text>
</TouchableOpacity>
</View>
</View>
),
headerRight: () => (
<View style={styles.headerRightContainer}>
{renderFollowButton()}
</View>
),
});
}, [post?.author, isFollowing, isFollowingMe, isFollowLoading, navigation, colors.background.default, styles]);
) : (
<View style={styles.headerPlaceholder} />
)}
<View style={styles.headerRightContainer}>
{renderFollowButton()}
</View>
</View>
);
// 监听键盘事件
useEffect(() => {
@@ -1076,12 +1063,15 @@ export const PostDetailScreen: React.FC = () => {
}
};
// 渲染关注按钮
const renderFollowButton = () => {
if (!post?.author?.id || currentUser?.id === post.author.id) return null;
// 渲染关注按钮(始终返回同尺寸占位,避免 header 右侧宽度跳动)
const renderFollowButton = useCallback(() => {
const shouldShow = post?.author?.id && currentUser?.id !== post.author.id;
if (!shouldShow) {
return <View style={styles.followButtonPlaceholder} />;
}
const config = getFollowButtonConfig();
return (
<TouchableOpacity
style={[
@@ -1107,7 +1097,7 @@ export const PostDetailScreen: React.FC = () => {
</Text>
</TouchableOpacity>
);
};
}, [post?.author?.id, currentUser?.id, isFollowing, isFollowingMe, isFollowLoading, styles]);
// 使用 useMemo 缓存图片数据,避免输入时重新创建数组导致图片重新加载
const postImages = useMemo(() => {
@@ -1712,7 +1702,9 @@ export const PostDetailScreen: React.FC = () => {
// 桌面端双栏布局
if (useDualColumnLayout) {
return (
<View style={styles.container}>
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="auto" />
{renderCustomHeader()}
<AdaptiveLayout
sidebar={renderSidebar()}
sidebarWidth={sidebarWidth}
@@ -1780,17 +1772,19 @@ export const PostDetailScreen: React.FC = () => {
onClose={() => setShowImageModal(false)}
enableSave
/>
</View>
</SafeAreaView>
);
}
// 移动端单栏布局
return (
<View style={styles.container}>
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="auto" />
{renderCustomHeader()}
<KeyboardAvoidingView
style={styles.flex}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={88}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : undefined}
>
<FlashList
ref={flatListRef}
@@ -1896,7 +1890,7 @@ export const PostDetailScreen: React.FC = () => {
}
}}
/>
</View>
</SafeAreaView>
);
};
@@ -1909,6 +1903,16 @@ function createPostDetailStyles(colors: AppColors) {
flex: 1,
backgroundColor: colors.background.default,
},
customHeader: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.default,
height: 52,
paddingRight: spacing.sm,
},
headerPlaceholder: {
flex: 1,
},
// Header 样式
headerContainer: {
flexDirection: 'row',
@@ -1932,10 +1936,12 @@ function createPostDetailStyles(colors: AppColors) {
flexDirection: 'row',
alignItems: 'center',
marginRight: spacing.sm,
backgroundColor: 'transparent',
},
headerBackButton: {
marginLeft: 0,
marginRight: 0,
backgroundColor: 'transparent',
},
// 帖子容器
postContainer: {
@@ -1950,6 +1956,7 @@ function createPostDetailStyles(colors: AppColors) {
minWidth: 64,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'transparent',
},
followButtonPrimary: {
backgroundColor: colors.primary.main,
@@ -1967,6 +1974,12 @@ function createPostDetailStyles(colors: AppColors) {
followButtonLoading: {
opacity: 0.7,
},
followButtonPlaceholder: {
minWidth: 64,
paddingHorizontal: spacing.md,
paddingVertical: 4,
backgroundColor: 'transparent',
},
followButtonText: {
fontSize: fontSizes.sm,
fontWeight: '600',