feat(profile): enhance settings and privacy UI with icons and theme picker
Refactor the profile settings and privacy screens to provide a more modern and intuitive user experience. - Add icons to visibility options and privacy settings items - Implement a theme selection dropdown in the settings screen - Restructure privacy settings layout into grouped containers - Move account management items (blocked users, deletion) from general settings to privacy settings - Improve visual hierarchy and spacing in settings screens
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* 采用扁平化设计,全宽布局,简洁分割线
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
@@ -34,6 +34,12 @@ 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;
|
||||
@@ -42,6 +48,7 @@ interface SettingsItem {
|
||||
showArrow?: boolean;
|
||||
danger?: boolean;
|
||||
subtitle?: string;
|
||||
isThemePicker?: boolean;
|
||||
}
|
||||
|
||||
const SETTINGS_GROUPS_BASE = [
|
||||
@@ -52,8 +59,6 @@ const SETTINGS_GROUPS_BASE = [
|
||||
{ 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 },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -87,6 +92,8 @@ function createSettingsStyles(colors: AppColors) {
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.divider,
|
||||
overflow: 'visible',
|
||||
zIndex: 1,
|
||||
},
|
||||
groupHeader: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
@@ -134,6 +141,53 @@ function createSettingsStyles(colors: AppColors) {
|
||||
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',
|
||||
@@ -172,12 +226,11 @@ export const SettingsScreen: React.FC = () => {
|
||||
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(() => {
|
||||
const themeSubtitle =
|
||||
themePreference === 'system' ? '跟随系统' : themePreference === 'dark' ? '深色' : '浅色';
|
||||
return [
|
||||
{
|
||||
title: '个性化',
|
||||
@@ -193,8 +246,7 @@ export const SettingsScreen: React.FC = () => {
|
||||
key: 'theme',
|
||||
title: '主题模式',
|
||||
icon: 'theme-light-dark',
|
||||
showArrow: true,
|
||||
subtitle: themeSubtitle,
|
||||
isThemePicker: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -208,31 +260,7 @@ export const SettingsScreen: React.FC = () => {
|
||||
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');
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
setOpenPicker(openPicker === 'theme' ? null : 'theme');
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -289,12 +317,19 @@ export const SettingsScreen: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderSettingItem = (item: SettingsItem, index: number, total: number) => (
|
||||
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 && styles.settingItemLast,
|
||||
index === total - 1 && !isThemeOpen && styles.settingItemLast,
|
||||
]}
|
||||
onPress={() => (item.onPress ? item.onPress() : handleItemPress(item.key))}
|
||||
activeOpacity={0.7}
|
||||
@@ -318,14 +353,64 @@ export const SettingsScreen: React.FC = () => {
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{item.showArrow && (
|
||||
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
|
||||
{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]) => (
|
||||
<View key={group.title}>
|
||||
const renderGroup = (group: (typeof settingsGroups)[0], groupIndex: number) => (
|
||||
<View
|
||||
key={group.title}
|
||||
style={[
|
||||
{ overflow: 'visible', zIndex: group.items.some((i) => i.isThemePicker && openPicker === 'theme') ? 10 : groupIndex },
|
||||
]}
|
||||
>
|
||||
<View style={styles.groupHeader}>
|
||||
<Text variant="caption" style={styles.groupTitle}>
|
||||
{group.title}
|
||||
@@ -352,7 +437,7 @@ export const SettingsScreen: React.FC = () => {
|
||||
|
||||
const renderContent = () => (
|
||||
<>
|
||||
{settingsGroups.map((group) => renderGroup(group))}
|
||||
{settingsGroups.map((group, index) => renderGroup(group, index))}
|
||||
{renderLogoutButton()}
|
||||
<View style={styles.footer}>
|
||||
<Text variant="caption" color={colors.text.hint}>
|
||||
|
||||
Reference in New Issue
Block a user