2026-03-09 21:29:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 设置页 SettingsScreen(响应式适配)
|
|
|
|
|
|
* 胡萝卜BBS - 应用设置
|
|
|
|
|
|
* 在宽屏下居中显示,最大宽度限制
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import React from 'react';
|
|
|
|
|
|
import {
|
|
|
|
|
|
View,
|
|
|
|
|
|
StyleSheet,
|
|
|
|
|
|
TouchableOpacity,
|
|
|
|
|
|
Alert,
|
|
|
|
|
|
ScrollView,
|
|
|
|
|
|
} from 'react-native';
|
|
|
|
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
2026-03-24 14:21:31 +08:00
|
|
|
|
import { useRouter } from 'expo-router';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
2026-03-20 19:28:42 +08:00
|
|
|
|
import Constants from 'expo-constants';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
|
|
|
|
|
|
import { useAuthStore } from '../../stores';
|
|
|
|
|
|
import { Text, ResponsiveContainer } from '../../components/common';
|
|
|
|
|
|
import { useResponsive } from '../../hooks';
|
2026-03-21 03:22:28 +08:00
|
|
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-03-24 14:21:31 +08:00
|
|
|
|
import * as hrefs from '../../navigation/hrefs';
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
interface SettingsItem {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
icon: string;
|
|
|
|
|
|
onPress?: () => void;
|
|
|
|
|
|
showArrow?: boolean;
|
|
|
|
|
|
danger?: boolean;
|
|
|
|
|
|
subtitle?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-20 19:28:42 +08:00
|
|
|
|
// 获取应用版本号
|
|
|
|
|
|
const APP_VERSION = Constants.expoConfig?.version || '1.0.0';
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
// 设置分组配置
|
|
|
|
|
|
const SETTINGS_GROUPS = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '账号与安全',
|
|
|
|
|
|
icon: 'shield-check-outline',
|
|
|
|
|
|
items: [
|
|
|
|
|
|
{ key: 'edit_profile', title: '编辑资料', icon: 'account-edit-outline', showArrow: true },
|
|
|
|
|
|
{ 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 },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '通知与通用',
|
|
|
|
|
|
icon: 'bell-outline',
|
|
|
|
|
|
items: [
|
|
|
|
|
|
{ key: 'notification_settings', title: '通知设置', icon: 'bell-cog-outline', showArrow: true, subtitle: '推送、震动、提示音' },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '关于与帮助',
|
|
|
|
|
|
icon: 'information-outline',
|
|
|
|
|
|
items: [
|
2026-03-20 19:28:42 +08:00
|
|
|
|
{ key: 'about', title: '关于我们', icon: 'information-outline', showArrow: true, subtitle: `版本 ${APP_VERSION}` },
|
2026-03-09 21:29:03 +08:00
|
|
|
|
{ key: 'help', title: '帮助与反馈', icon: 'help-circle-outline', showArrow: true },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
export const SettingsScreen: React.FC = () => {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
const router = useRouter();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const { logout } = useAuthStore();
|
|
|
|
|
|
const { isWideScreen } = useResponsive();
|
2026-03-21 03:22:28 +08:00
|
|
|
|
const insets = useSafeAreaInsets();
|
|
|
|
|
|
const { isMobile } = useResponsive();
|
|
|
|
|
|
|
|
|
|
|
|
// 底部间距,避免被 TabBar 遮挡
|
|
|
|
|
|
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 处理设置项点击
|
|
|
|
|
|
const handleItemPress = (key: string) => {
|
|
|
|
|
|
switch (key) {
|
|
|
|
|
|
case 'edit_profile':
|
2026-03-24 14:21:31 +08:00
|
|
|
|
router.push(hrefs.hrefProfileEdit());
|
2026-03-09 21:29:03 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 'notification_settings':
|
2026-03-24 14:21:31 +08:00
|
|
|
|
router.push(hrefs.hrefProfileNotifications());
|
2026-03-09 21:29:03 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 'blocked_users':
|
2026-03-24 14:21:31 +08:00
|
|
|
|
router.push(hrefs.hrefProfileBlocked());
|
2026-03-09 21:29:03 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 'security':
|
2026-03-24 14:21:31 +08:00
|
|
|
|
router.push(hrefs.hrefProfileSecurity());
|
2026-03-09 21:29:03 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 'logout':
|
|
|
|
|
|
Alert.alert(
|
|
|
|
|
|
'退出登录',
|
|
|
|
|
|
'确定要退出登录吗?',
|
|
|
|
|
|
[
|
|
|
|
|
|
{ text: '取消', style: 'cancel' },
|
|
|
|
|
|
{
|
|
|
|
|
|
text: '确定',
|
|
|
|
|
|
style: 'destructive',
|
2026-03-24 14:21:31 +08:00
|
|
|
|
onPress: async () => {
|
|
|
|
|
|
await logout();
|
|
|
|
|
|
router.replace(hrefs.hrefAuthLogin());
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
);
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
2026-03-09 22:18:47 +08:00
|
|
|
|
break;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染单个设置项
|
|
|
|
|
|
const renderSettingItem = (item: SettingsItem, index: number, total: number) => (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
key={item.key}
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.settingItem,
|
|
|
|
|
|
index === 0 && styles.settingItemFirst,
|
|
|
|
|
|
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.primary.main}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<View style={styles.settingContent}>
|
|
|
|
|
|
<Text
|
|
|
|
|
|
variant="body"
|
|
|
|
|
|
color={item.danger ? colors.error.main : colors.text.primary}
|
|
|
|
|
|
>
|
|
|
|
|
|
{item.title}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
{item.subtitle && (
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary} 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 SETTINGS_GROUPS[0], groupIndex: number) => (
|
|
|
|
|
|
<View key={group.title} style={styles.groupContainer}>
|
|
|
|
|
|
<View style={styles.groupHeader}>
|
|
|
|
|
|
<MaterialCommunityIcons name={group.icon as any} size={16} color={colors.primary.main} />
|
|
|
|
|
|
<Text variant="caption" color={colors.text.secondary} style={styles.groupTitle}>
|
|
|
|
|
|
{group.title}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<View style={styles.card}>
|
|
|
|
|
|
{group.items.map((item, index) =>
|
|
|
|
|
|
renderSettingItem(item, index, group.items.length)
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 退出登录按钮
|
|
|
|
|
|
const renderLogoutButton = () => (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={styles.logoutButton}
|
|
|
|
|
|
onPress={() => handleItemPress('logout')}
|
|
|
|
|
|
activeOpacity={0.7}
|
|
|
|
|
|
>
|
|
|
|
|
|
<MaterialCommunityIcons name="logout-variant" size={20} color={colors.error.main} />
|
|
|
|
|
|
<Text variant="body" color={colors.error.main} style={styles.logoutText}>
|
|
|
|
|
|
退出登录
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染内容
|
|
|
|
|
|
const renderContent = () => (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{SETTINGS_GROUPS.map((group, index) => renderGroup(group, index))}
|
|
|
|
|
|
{renderLogoutButton()}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 底部版权信息 */}
|
|
|
|
|
|
<View style={styles.footer}>
|
|
|
|
|
|
<Text variant="caption" color={colors.text.hint}>
|
2026-03-20 19:28:42 +08:00
|
|
|
|
萝卜社区 v{APP_VERSION}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
</Text>
|
|
|
|
|
|
<Text variant="caption" color={colors.text.hint} style={styles.copyright}>
|
|
|
|
|
|
© 2024 Carrot BBS. All rights reserved.
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
|
|
|
|
|
{isWideScreen ? (
|
|
|
|
|
|
<ResponsiveContainer maxWidth={800}>
|
2026-03-21 03:22:28 +08:00
|
|
|
|
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset }]}>
|
2026-03-09 21:29:03 +08:00
|
|
|
|
{renderContent()}
|
|
|
|
|
|
</ScrollView>
|
|
|
|
|
|
</ResponsiveContainer>
|
|
|
|
|
|
) : (
|
2026-03-21 03:22:28 +08:00
|
|
|
|
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset }]}>
|
2026-03-09 21:29:03 +08:00
|
|
|
|
{renderContent()}
|
|
|
|
|
|
</ScrollView>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
|
|
|
container: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
backgroundColor: colors.background.default,
|
|
|
|
|
|
},
|
|
|
|
|
|
scrollContent: {
|
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
groupContainer: {
|
|
|
|
|
|
marginBottom: spacing.lg,
|
|
|
|
|
|
},
|
|
|
|
|
|
groupHeader: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
paddingHorizontal: spacing.lg,
|
|
|
|
|
|
marginBottom: spacing.sm,
|
|
|
|
|
|
gap: spacing.xs,
|
|
|
|
|
|
},
|
|
|
|
|
|
groupTitle: {
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
fontSize: fontSizes.sm,
|
|
|
|
|
|
},
|
|
|
|
|
|
card: {
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
marginHorizontal: spacing.lg,
|
|
|
|
|
|
borderRadius: borderRadius.lg,
|
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
|
},
|
|
|
|
|
|
settingItem: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
|
paddingHorizontal: spacing.md,
|
|
|
|
|
|
borderBottomWidth: 1,
|
|
|
|
|
|
borderBottomColor: colors.divider,
|
|
|
|
|
|
},
|
|
|
|
|
|
settingItemFirst: {
|
|
|
|
|
|
borderTopLeftRadius: borderRadius.lg,
|
|
|
|
|
|
borderTopRightRadius: borderRadius.lg,
|
|
|
|
|
|
},
|
|
|
|
|
|
settingItemLast: {
|
|
|
|
|
|
borderBottomLeftRadius: borderRadius.lg,
|
|
|
|
|
|
borderBottomRightRadius: borderRadius.lg,
|
|
|
|
|
|
borderBottomWidth: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
settingItemLeft: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
iconContainer: {
|
|
|
|
|
|
width: 36,
|
|
|
|
|
|
height: 36,
|
|
|
|
|
|
borderRadius: borderRadius.md,
|
|
|
|
|
|
backgroundColor: colors.primary.light + '20',
|
|
|
|
|
|
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',
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
marginHorizontal: spacing.lg,
|
|
|
|
|
|
marginTop: spacing.sm,
|
|
|
|
|
|
marginBottom: spacing.lg,
|
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
|
borderRadius: borderRadius.lg,
|
|
|
|
|
|
gap: spacing.sm,
|
|
|
|
|
|
},
|
|
|
|
|
|
logoutText: {
|
|
|
|
|
|
fontWeight: '500',
|
|
|
|
|
|
},
|
|
|
|
|
|
footer: {
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
marginTop: spacing.xl,
|
|
|
|
|
|
paddingBottom: spacing.xl,
|
|
|
|
|
|
},
|
|
|
|
|
|
copyright: {
|
|
|
|
|
|
marginTop: spacing.xs,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
export default SettingsScreen;
|