Add dedicated Help screen to profile tab with navigation integration.
Refactor SettingsScreen to navigate to HelpScreen instead of showing placeholder alert.
Fix API upload method to use correct field names ("file" for documents, "image" for media)
based on endpoint path, resolving upload failures for file uploads.
465 lines
14 KiB
TypeScript
465 lines
14 KiB
TypeScript
/**
|
||
* 设置页 SettingsScreen - Twitter/X 风格
|
||
* 威友 - 应用设置
|
||
* 采用扁平化设计,全宽布局,简洁分割线
|
||
*/
|
||
|
||
import React, { useMemo, useState } 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';
|
||
|
||
const THEME_OPTIONS: { value: 'system' | 'light' | 'dark'; label: string }[] = [
|
||
{ value: 'system', label: '跟随系统' },
|
||
{ value: 'light', label: '浅色' },
|
||
{ value: 'dark', label: '深色' },
|
||
];
|
||
|
||
interface SettingsItem {
|
||
key: string;
|
||
title: string;
|
||
icon: string;
|
||
onPress?: () => void;
|
||
showArrow?: boolean;
|
||
danger?: boolean;
|
||
subtitle?: string;
|
||
isThemePicker?: boolean;
|
||
}
|
||
|
||
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 },
|
||
],
|
||
},
|
||
{
|
||
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,
|
||
overflow: 'visible',
|
||
zIndex: 1,
|
||
},
|
||
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,
|
||
},
|
||
// 无背景色的选择框,紧贴右侧
|
||
selectBox: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingVertical: spacing.xs,
|
||
},
|
||
selectBoxText: {
|
||
fontSize: fontSizes.sm,
|
||
color: colors.text.secondary,
|
||
marginRight: spacing.xs,
|
||
},
|
||
// 原位展开的下拉菜单
|
||
dropdownMenu: {
|
||
position: 'absolute',
|
||
right: 0,
|
||
top: '100%',
|
||
marginTop: 4,
|
||
backgroundColor: colors.background.paper,
|
||
borderRadius: borderRadius.md,
|
||
borderWidth: StyleSheet.hairlineWidth,
|
||
borderColor: colors.divider,
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 2 },
|
||
shadowOpacity: 0.1,
|
||
shadowRadius: 4,
|
||
elevation: 4,
|
||
zIndex: 100,
|
||
minWidth: 120,
|
||
overflow: 'hidden',
|
||
},
|
||
dropdownOption: {
|
||
paddingHorizontal: spacing.md,
|
||
paddingVertical: spacing.sm,
|
||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||
borderBottomColor: colors.divider,
|
||
},
|
||
dropdownOptionLast: {
|
||
borderBottomWidth: 0,
|
||
},
|
||
dropdownOptionText: {
|
||
fontSize: fontSizes.sm,
|
||
color: colors.text.primary,
|
||
},
|
||
dropdownOptionTextActive: {
|
||
color: colors.primary.main,
|
||
fontWeight: '600',
|
||
},
|
||
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((s) => s.logout);
|
||
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 [openPicker, setOpenPicker] = useState<string | null>(null);
|
||
|
||
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
|
||
|
||
const settingsGroups = useMemo(() => {
|
||
return [
|
||
{
|
||
title: '个性化',
|
||
items: [
|
||
{
|
||
key: 'chat_settings',
|
||
title: '聊天设置',
|
||
icon: 'message-text-outline',
|
||
showArrow: true,
|
||
subtitle: '字号、主题、壁纸',
|
||
},
|
||
{
|
||
key: 'theme',
|
||
title: '主题模式',
|
||
icon: 'theme-light-dark',
|
||
isThemePicker: true,
|
||
},
|
||
],
|
||
},
|
||
...SETTINGS_GROUPS_BASE,
|
||
];
|
||
}, [themePreference]);
|
||
|
||
const handleItemPress = (key: string) => {
|
||
switch (key) {
|
||
case 'chat_settings':
|
||
router.push(hrefs.hrefProfileChatSettings());
|
||
break;
|
||
case 'theme': {
|
||
setOpenPicker(openPicker === 'theme' ? null : 'theme');
|
||
break;
|
||
}
|
||
|
||
case 'data_storage':
|
||
router.push(hrefs.hrefProfileDataStorage());
|
||
break;
|
||
case 'about':
|
||
router.push(hrefs.hrefProfileAbout());
|
||
break;
|
||
case 'help':
|
||
router.push(hrefs.hrefProfileHelp());
|
||
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 getThemeLabel = (value: string) =>
|
||
THEME_OPTIONS.find((o) => o.value === value)?.label ?? '跟随系统';
|
||
|
||
const renderSettingItem = (item: SettingsItem, index: number, total: number) => {
|
||
const isThemeOpen = item.isThemePicker && openPicker === 'theme';
|
||
const themeLabel = item.isThemePicker ? getThemeLabel(themePreference) : undefined;
|
||
|
||
return (
|
||
<TouchableOpacity
|
||
key={item.key}
|
||
style={[
|
||
styles.settingItem,
|
||
index === total - 1 && !isThemeOpen && 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.isThemePicker ? (
|
||
<View style={{ position: 'relative' }}>
|
||
<View style={styles.selectBox}>
|
||
<Text style={styles.selectBoxText}>{themeLabel}</Text>
|
||
<MaterialCommunityIcons
|
||
name={isThemeOpen ? 'chevron-up' : 'chevron-down'}
|
||
size={18}
|
||
color={colors.text.secondary}
|
||
/>
|
||
</View>
|
||
{isThemeOpen && (
|
||
<View style={styles.dropdownMenu}>
|
||
{THEME_OPTIONS.map((option, optIndex) => {
|
||
const isActive = themePreference === option.value;
|
||
return (
|
||
<TouchableOpacity
|
||
key={option.value}
|
||
style={[
|
||
styles.dropdownOption,
|
||
optIndex === THEME_OPTIONS.length - 1 && styles.dropdownOptionLast,
|
||
]}
|
||
onPress={() => {
|
||
void setThemePreference(option.value);
|
||
setOpenPicker(null);
|
||
}}
|
||
activeOpacity={0.7}
|
||
>
|
||
<Text
|
||
variant="body"
|
||
style={[
|
||
styles.dropdownOptionText,
|
||
isActive ? styles.dropdownOptionTextActive : undefined,
|
||
]}
|
||
>
|
||
{option.label}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
);
|
||
})}
|
||
</View>
|
||
)}
|
||
</View>
|
||
) : (
|
||
item.showArrow && (
|
||
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
|
||
)
|
||
)}
|
||
</TouchableOpacity>
|
||
);
|
||
};
|
||
|
||
const renderGroup = (group: (typeof settingsGroups)[0], groupIndex: number) => (
|
||
<View
|
||
key={group.title}
|
||
style={[
|
||
{ overflow: 'visible', zIndex: group.items.some((i) => 'isThemePicker' in i && i.isThemePicker && openPicker === 'theme') ? 10 : groupIndex },
|
||
]}
|
||
>
|
||
<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, index) => renderGroup(group, index))}
|
||
{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;
|