Files
frontend/src/screens/profile/SettingsScreen.tsx
lan 4213d13b8f
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(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.
2026-05-03 22:01:43 +08:00

380 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 设置页 SettingsScreen - Twitter/X 风格
* 威友 - 应用设置
* 采用扁平化设计,全宽布局,简洁分割线
*/
import React, { useMemo } from 'react';
import {
View,
StyleSheet,
TouchableOpacity,
Alert,
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';
import {
useAppColors,
spacing,
fontSizes,
borderRadius,
useThemePreference,
useSetThemePreference,
type AppColors,
} from '../../theme';
import { useAuthStore } from '../../stores';
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';
const APP_VERSION = Constants.expoConfig?.version || '1.0.0';
interface SettingsItem {
key: string;
title: string;
icon: string;
onPress?: () => void;
showArrow?: boolean;
danger?: boolean;
subtitle?: string;
}
const SETTINGS_GROUPS_BASE = [
{
title: '账号与安全',
items: [
{ key: 'edit_profile', title: '编辑资料', icon: 'account-edit-outline', showArrow: true },
{ key: 'verification', title: '身份认证', icon: 'check-decagram', showArrow: true, subtitle: '获得认证标识,解锁更多功能' },
{ key: 'privacy', title: '隐私设置', icon: 'shield-account-outline', showArrow: true },
{ key: 'security', title: '账号安全', icon: 'lock-outline', showArrow: true },
{ key: 'blocked_users', title: '黑名单', icon: 'account-off-outline', showArrow: true },
{ key: 'account_deletion', title: '注销账号', icon: 'account-remove-outline', showArrow: true, danger: true },
],
},
{
title: '通知与通用',
items: [
{ key: 'notification_settings', title: '通知设置', icon: 'bell-cog-outline', showArrow: true, subtitle: '推送、震动、提示音' },
{ key: 'data_storage', title: '数据与存储', icon: 'database-outline', showArrow: true },
],
},
{
title: '关于与帮助',
items: [
{ key: 'about', title: '关于我们', icon: 'information-outline', showArrow: true, subtitle: `版本 ${APP_VERSION}` },
{ key: 'help', title: '帮助与反馈', icon: 'help-circle-outline', showArrow: true },
],
},
];
function createSettingsStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollContent: {
paddingVertical: spacing.sm,
},
groupContainer: {
marginBottom: spacing.xl,
backgroundColor: colors.background.paper,
borderTopWidth: StyleSheet.hairlineWidth,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: colors.divider,
},
groupHeader: {
paddingHorizontal: spacing.lg,
paddingVertical: spacing.sm,
backgroundColor: colors.background.default,
},
groupTitle: {
fontWeight: '600',
fontSize: fontSizes.sm,
color: colors.text.secondary,
},
settingItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 14,
paddingHorizontal: spacing.lg,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
backgroundColor: colors.background.paper,
},
settingItemLast: {
borderBottomWidth: 0,
},
settingItemLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
iconContainer: {
width: 32,
height: 32,
borderRadius: borderRadius.md,
backgroundColor: colors.background.default,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.md,
},
dangerIconContainer: {
backgroundColor: colors.error.light + '20',
},
settingContent: {
flex: 1,
},
subtitle: {
marginTop: 2,
},
logoutButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginTop: spacing.lg,
marginBottom: spacing.xl,
paddingVertical: 14,
marginHorizontal: spacing.lg,
borderRadius: borderRadius.full,
gap: spacing.sm,
backgroundColor: colors.background.paper,
borderWidth: 1,
borderColor: colors.divider,
},
logoutText: {
fontWeight: '700',
},
footer: {
alignItems: 'center',
marginTop: spacing.md,
paddingBottom: spacing.xl,
},
copyright: {
marginTop: spacing.xs,
},
});
}
export const SettingsScreen: React.FC = () => {
const router = useRouter();
const { logout } = useAuthStore();
const insets = useSafeAreaInsets();
const { isMobile } = useResponsive();
const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 });
const colors = useAppColors();
const styles = useMemo(() => createSettingsStyles(colors), [colors]);
const themePreference = useThemePreference();
const setThemePreference = useSetThemePreference();
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
const settingsGroups = useMemo(() => {
const themeSubtitle =
themePreference === 'system' ? '跟随系统' : themePreference === 'dark' ? '深色' : '浅色';
return [
{
title: '个性化',
items: [
{
key: 'chat_settings',
title: '聊天设置',
icon: 'message-text-outline',
showArrow: true,
subtitle: '字号、主题、壁纸',
},
{
key: 'theme',
title: '主题模式',
icon: 'theme-light-dark',
showArrow: true,
subtitle: themeSubtitle,
},
],
},
...SETTINGS_GROUPS_BASE,
];
}, [themePreference]);
const handleItemPress = (key: string) => {
switch (key) {
case 'chat_settings':
router.push(hrefs.hrefProfileChatSettings());
break;
case 'theme': {
Alert.alert(
'主题模式',
'选择应用界面明暗',
[
{ text: '取消', style: 'cancel' },
{
text: '跟随系统',
onPress: () => {
void setThemePreference('system');
},
},
{
text: '浅色',
onPress: () => {
void setThemePreference('light');
},
},
{
text: '深色',
onPress: () => {
void setThemePreference('dark');
},
},
]
);
break;
}
case 'data_storage':
router.push(hrefs.hrefProfileDataStorage());
break;
case 'about':
router.push(hrefs.hrefProfileAbout());
break;
case 'help':
Alert.alert('帮助与反馈', '帮助中心即将上线!');
break;
case 'edit_profile':
router.push(hrefs.hrefProfileEdit());
break;
case 'verification':
router.push(hrefs.hrefProfileVerification());
break;
case 'notification_settings':
router.push(hrefs.hrefProfileNotifications());
break;
case 'blocked_users':
router.push(hrefs.hrefProfileBlocked());
break;
case 'security':
router.push(hrefs.hrefProfileSecurity());
break;
case 'privacy':
router.push(hrefs.hrefProfilePrivacySettings());
break;
case 'account_deletion':
router.push(hrefs.hrefProfileDeletion());
break;
case 'logout':
Alert.alert(
'退出登录',
'确定要退出登录吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
style: 'destructive',
onPress: async () => {
await logout();
router.replace(hrefs.hrefAuthLogin());
},
},
]
);
break;
default:
break;
}
};
const renderSettingItem = (item: SettingsItem, index: number, total: number) => (
<TouchableOpacity
key={item.key}
style={[
styles.settingItem,
index === total - 1 && styles.settingItemLast,
]}
onPress={() => (item.onPress ? item.onPress() : handleItemPress(item.key))}
activeOpacity={0.7}
>
<View style={styles.settingItemLeft}>
<View style={[styles.iconContainer, item.danger && styles.dangerIconContainer]}>
<MaterialCommunityIcons
name={item.icon as any}
size={20}
color={item.danger ? colors.error.main : colors.text.secondary}
/>
</View>
<View style={styles.settingContent}>
<Text variant="body" color={item.danger ? colors.error.main : colors.text.primary} style={{ fontWeight: '500' }}>
{item.title}
</Text>
{item.subtitle && (
<Text variant="caption" color={colors.text.hint} style={styles.subtitle}>
{item.subtitle}
</Text>
)}
</View>
</View>
{item.showArrow && (
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
)}
</TouchableOpacity>
);
const renderGroup = (group: (typeof settingsGroups)[0]) => (
<View key={group.title}>
<View style={styles.groupHeader}>
<Text variant="caption" style={styles.groupTitle}>
{group.title}
</Text>
</View>
<View style={styles.groupContainer}>
{group.items.map((item, index) => renderSettingItem(item, index, group.items.length))}
</View>
</View>
);
const renderLogoutButton = () => (
<TouchableOpacity
style={styles.logoutButton}
onPress={() => handleItemPress('logout')}
activeOpacity={0.8}
>
<MaterialCommunityIcons name="logout-variant" size={18} color={colors.error.main} />
<Text variant="body" color={colors.error.main} style={styles.logoutText}>
退
</Text>
</TouchableOpacity>
);
const renderContent = () => (
<>
{settingsGroups.map((group) => renderGroup(group))}
{renderLogoutButton()}
<View style={styles.footer}>
<Text variant="caption" color={colors.text.hint}>
v{APP_VERSION}
</Text>
<Text variant="caption" color={colors.text.hint} style={styles.copyright}>
© 2026
</Text>
</View>
</>
);
return (
<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>
</SafeAreaView>
);
};
export default SettingsScreen;