refactor(ui): implement custom header system and unify screen layouts
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:
@@ -50,6 +50,7 @@ const styles = StyleSheet.create({
|
||||
marginLeft: -12,
|
||||
marginRight: 0,
|
||||
padding: 0,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
55
src/components/common/SimpleHeader.tsx
Normal file
55
src/components/common/SimpleHeader.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { useAppColors, spacing } from '../../theme';
|
||||
import { AppBackButton, Text } from './index';
|
||||
|
||||
interface SimpleHeaderProps {
|
||||
title: string;
|
||||
onBack: () => void;
|
||||
rightComponent?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SimpleHeader: React.FC<SimpleHeaderProps> = ({ title, onBack, rightComponent }) => {
|
||||
const colors = useAppColors();
|
||||
|
||||
return (
|
||||
<View style={[styles.header, { backgroundColor: colors.background.paper }]}>
|
||||
<AppBackButton onPress={onBack} style={styles.backButton} />
|
||||
<Text style={[styles.title, { color: colors.text.primary }]} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
{rightComponent ? (
|
||||
<View style={styles.rightContainer}>
|
||||
{rightComponent}
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.rightPlaceholder} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
height: 52,
|
||||
paddingHorizontal: spacing.xs,
|
||||
},
|
||||
title: {
|
||||
flex: 1,
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
rightContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
rightPlaceholder: {
|
||||
width: 40,
|
||||
},
|
||||
backButton: {
|
||||
marginLeft: 0,
|
||||
},
|
||||
});
|
||||
@@ -18,6 +18,7 @@ export { default as ResponsiveGrid } from './ResponsiveGrid';
|
||||
export { default as ResponsiveStack, HStack, VStack } from './ResponsiveStack';
|
||||
export { default as AdaptiveLayout, SidebarLayout } from './AdaptiveLayout';
|
||||
export { default as AppBackButton } from './AppBackButton';
|
||||
export { SimpleHeader } from './SimpleHeader';
|
||||
export { default as PagerView } from './PagerView';
|
||||
|
||||
// 图片相关组件
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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}>
|
||||
{/* 邮箱验证分组 */}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user