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

@@ -18,6 +18,7 @@ import {
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import Constants from 'expo-constants';
import { StatusBar } from 'expo-status-bar';
import { useRouter } from 'expo-router';
import {
useAppColors,
@@ -26,7 +27,7 @@ import {
borderRadius,
type AppColors,
} from '../../theme';
import { Text } from '../../components/common';
import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks';
import { checkForAPKUpdate, type VersionCheckResult } from '@/services/platform';
import * as hrefs from '../../navigation/hrefs';
@@ -517,7 +518,9 @@ export const AboutScreen: React.FC = () => {
);
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="关于我们" onBack={() => router.back()} />
<ScrollView
contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}
showsVerticalScrollIndicator={false}

View File

@@ -1,3 +1,4 @@
import { StatusBar } from 'expo-status-bar';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
@@ -13,7 +14,7 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { authService } from '../../services';
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
import { Text } from '../../components/common';
import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks';
import { useAuthStore } from '../../stores';
import type { DeletionStatusDTO } from '../../types/dto';
@@ -275,7 +276,7 @@ export const AccountDeletionScreen: React.FC = () => {
if (loading) {
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.loadingWrap}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
@@ -284,7 +285,9 @@ export const AccountDeletionScreen: React.FC = () => {
}
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="注销账号" onBack={() => router.back()} />
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
<View style={styles.content}>
{status?.is_pending_deletion ? (

View File

@@ -7,10 +7,12 @@ import {
ActivityIndicator,
ScrollView,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { useRouter } from 'expo-router';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
import { Text } from '../../components/common';
import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks';
// 内容最大宽度
@@ -21,6 +23,7 @@ import { showPrompt } from '@/services/ui';
import { useAuthStore } from '../../stores';
export const AccountSecurityScreen: React.FC = () => {
const router = useRouter();
const colors = useAppColors();
const styles = useMemo(() => createAccountSecurityStyles(colors), [colors]);
const { isWideScreen, isMobile } = useResponsive();
@@ -202,7 +205,9 @@ export const AccountSecurityScreen: React.FC = () => {
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="账号安全" onBack={() => router.back()} />
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
<View style={styles.content}>
{/* 邮箱验证分组 */}

View File

@@ -10,7 +10,8 @@ import {
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { Avatar, Button, EmptyState, ResponsiveContainer, Text } from '../../components/common';
import { StatusBar } from 'expo-status-bar';
import { Avatar, Button, EmptyState, ResponsiveContainer, SimpleHeader, Text } from '../../components/common';
import { authService } from '../../services';
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
import { User } from '../../types';
@@ -140,7 +141,8 @@ export const BlockedUsersScreen: React.FC = () => {
if (loading) {
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<View style={styles.loadingWrap}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
@@ -149,7 +151,9 @@ export const BlockedUsersScreen: React.FC = () => {
}
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="黑名单" onBack={() => router.back()} />
<ResponsiveContainer maxWidth={900}>
<FlatList
data={users}

View File

@@ -12,7 +12,9 @@ import {
Dimensions,
PanResponder,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import {
useAppColors,
spacing,
@@ -20,7 +22,7 @@ import {
borderRadius,
type AppColors,
} from '../../theme';
import { Text } from '../../components/common';
import { Text, SimpleHeader } from '../../components/common';
import { useResponsiveSpacing } from '../../hooks';
import {
useChatSettingsStore,
@@ -265,6 +267,7 @@ function createStyles(colors: AppColors) {
}
export const ChatSettingsScreen: React.FC = () => {
const router = useRouter();
const colors = useAppColors();
const styles = useMemo(() => createStyles(colors), [colors]);
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
@@ -379,7 +382,9 @@ export const ChatSettingsScreen: React.FC = () => {
);
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="聊天设置" onBack={() => router.back()} />
<ScrollView
contentContainerStyle={[
styles.scrollContent,

View File

@@ -14,12 +14,14 @@ import {
Platform,
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { StatusBar } from 'expo-status-bar';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Image } from 'expo-image';
import { Directory, File, Paths } from 'expo-file-system';
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
import { Text } from '../../components/common';
import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks';
import { mediaCacheManager } from '../../infrastructure/cache';
import type { CacheStats } from '../../infrastructure/cache';
@@ -42,6 +44,7 @@ interface StorageStatItem {
}
export const DataStorageScreen: React.FC = () => {
const router = useRouter();
const colors = useAppColors();
const styles = useMemo(() => createDataStorageStyles(colors), [colors]);
const { isMobile } = useResponsive();
@@ -372,7 +375,9 @@ export const DataStorageScreen: React.FC = () => {
};
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="数据与存储" onBack={() => router.back()} />
<ScrollView
contentContainerStyle={[
styles.scrollContent,

View File

@@ -18,6 +18,8 @@ import {
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
import { useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import {
@@ -28,7 +30,7 @@ import {
type AppColors,
} from '../../theme';
import { useAuthStore } from '../../stores';
import { Avatar, Text } from '../../components/common';
import { Avatar, Text, SimpleHeader } from '../../components/common';
import { authService, uploadService } from '../../services';
import { useResponsive, useResponsiveSpacing } from '../../hooks';
@@ -276,6 +278,7 @@ export const EditProfileScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createEditProfileStyles(colors), [colors]);
const navigation = useNavigation();
const router = useRouter();
const { currentUser, updateUser } = useAuthStore();
const { isWideScreen, isMobile, width } = useResponsive();
const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 });
@@ -596,7 +599,9 @@ export const EditProfileScreen: React.FC = () => {
);
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="编辑资料" onBack={() => router.back()} />
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardView}

View File

@@ -4,6 +4,7 @@
* 在宽屏下居中显示
*/
import { useRouter } from 'expo-router';
import React, { useState, useEffect, useMemo } from 'react';
import {
View,
@@ -14,10 +15,11 @@ import {
TouchableOpacity,
Platform,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
import { Text } from '../../components/common';
import { Text, SimpleHeader } from '../../components/common';
import {
loadNotificationPreferences,
setPushNotificationsPreference,
@@ -41,6 +43,7 @@ interface NotificationSettingItem {
}
export const NotificationSettingsScreen: React.FC = () => {
const router = useRouter();
const colors = useAppColors();
const styles = useMemo(() => createNotificationSettingsStyles(colors), [colors]);
const [vibrationEnabled, setVibrationEnabledState] = useState(true);
@@ -316,7 +319,9 @@ export const NotificationSettingsScreen: React.FC = () => {
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="通知设置" onBack={() => router.back()} />
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
{renderContent()}
</ScrollView>

View File

@@ -10,13 +10,15 @@ import {
StyleSheet,
ScrollView,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { useRouter } from 'expo-router';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import {
useAppColors,
spacing,
type AppColors,
} from '../../theme';
import { Text } from '../../components/common';
import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks';
// 内容最大宽度
@@ -278,6 +280,7 @@ export const PrivacyPolicyScreen: React.FC = () => {
const colors = useAppColors();
const styles = useMemo(() => createPrivacyStyles(colors), [colors]);
const insets = useSafeAreaInsets();
const router = useRouter();
const { isMobile } = useResponsive();
const responsivePadding = useResponsiveSpacing({ xs: 20, sm: 24, md: 28, lg: 32, xl: 40 });
@@ -285,7 +288,9 @@ export const PrivacyPolicyScreen: React.FC = () => {
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.lg;
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="隐私政策" onBack={() => router.back()} />
<ScrollView
contentContainerStyle={[styles.scrollContent, { paddingHorizontal: responsivePadding, paddingBottom: scrollBottomInset }]}
showsVerticalScrollIndicator={false}

View File

@@ -7,12 +7,13 @@ import {
TouchableOpacity,
View,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { authService } from '../../services';
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
import { Text } from '../../components/common';
import { SimpleHeader, Text } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks';
import type { PrivacySettingsDTO, VisibilityLevel } from '../../types/dto';
@@ -200,7 +201,8 @@ export const PrivacySettingsScreen: React.FC = () => {
if (loading) {
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<View style={styles.loadingWrap}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
@@ -209,7 +211,9 @@ export const PrivacySettingsScreen: React.FC = () => {
}
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="隐私设置" onBack={() => router.back()} />
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
<View style={styles.content}>
<View style={styles.section}>

View File

@@ -13,6 +13,7 @@ import {
ScrollView,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { StatusBar } from 'expo-status-bar';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import Constants from 'expo-constants';
@@ -26,7 +27,7 @@ import {
type AppColors,
} from '../../theme';
import { useAuthStore } from '../../stores';
import { Text } from '../../components/common';
import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as hrefs from '../../navigation/hrefs';
@@ -365,7 +366,9 @@ export const SettingsScreen: React.FC = () => {
);
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="设置" onBack={() => router.back()} />
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
{renderContent()}
</ScrollView>

View File

@@ -10,13 +10,15 @@ import {
StyleSheet,
ScrollView,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import {
useAppColors,
spacing,
type AppColors,
} from '../../theme';
import { Text } from '../../components/common';
import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks';
// 内容最大宽度
@@ -213,12 +215,15 @@ export const TermsOfServiceScreen: React.FC = () => {
const insets = useSafeAreaInsets();
const { isMobile } = useResponsive();
const responsivePadding = useResponsiveSpacing({ xs: 20, sm: 24, md: 28, lg: 32, xl: 40 });
const router = useRouter();
// 底部间距,避免被 TabBar 遮挡
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.lg;
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="用户协议" onBack={() => router.back()} />
<ScrollView
contentContainerStyle={[styles.scrollContent, { paddingHorizontal: responsivePadding, paddingBottom: scrollBottomInset }]}
showsVerticalScrollIndicator={false}

View File

@@ -5,22 +5,28 @@
*/
import React from 'react';
import { useLocalSearchParams } from 'expo-router';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
import UserProfileScreen from './UserProfileScreen';
import { useCurrentUser } from '../../stores/auth';
import { SimpleHeader } from '../../components/common';
export const UserScreen: React.FC = () => {
const { userId: userIdParam } = useLocalSearchParams<{ userId?: string }>();
const router = useRouter();
const userId = userIdParam || '';
const currentUser = useCurrentUser();
const isSelfProfile = !!currentUser?.id && currentUser.id === userId;
return (
<UserProfileScreen
mode={isSelfProfile ? 'self' : 'other'}
userId={isSelfProfile ? undefined : userId}
hasHeader
/>
<SafeAreaView style={{ flex: 1 }} edges={['top', 'bottom']}>
<SimpleHeader title="用户主页" onBack={() => router.back()} />
<UserProfileScreen
mode={isSelfProfile ? 'self' : 'other'}
userId={isSelfProfile ? undefined : userId}
hasHeader={false}
/>
</SafeAreaView>
);
};

View File

@@ -14,8 +14,10 @@ import {
Alert,
ActivityIndicator,
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { SimpleHeader } from '../../components/common';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useAppColors, type AppColors } from '../../theme';
import {
@@ -91,7 +93,9 @@ export const VerificationSettingsScreen: React.FC = () => {
if (loading && !status) {
return (
<SafeAreaView style={styles.container}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="身份认证" onBack={() => router.back()} />
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
@@ -104,7 +108,9 @@ export const VerificationSettingsScreen: React.FC = () => {
const identityText = IDENTITY_TEXT[status?.identity || 'general'];
return (
<SafeAreaView style={styles.container}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="身份认证" onBack={() => router.back()} />
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}