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:
@@ -12,31 +12,43 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
|||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { authService } from '../../services';
|
import { authService } from '../../services';
|
||||||
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
|
import {
|
||||||
|
spacing,
|
||||||
|
borderRadius,
|
||||||
|
fontSizes,
|
||||||
|
useAppColors,
|
||||||
|
type AppColors,
|
||||||
|
} from '../../theme';
|
||||||
import { SimpleHeader, Text } from '../../components/common';
|
import { SimpleHeader, Text } from '../../components/common';
|
||||||
import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
||||||
import type { PrivacySettingsDTO, VisibilityLevel } from '../../types/dto';
|
import type { PrivacySettingsDTO, VisibilityLevel } from '../../types/dto';
|
||||||
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
|
|
||||||
const CONTENT_MAX_WIDTH = 720;
|
const CONTENT_MAX_WIDTH = 720;
|
||||||
|
|
||||||
const VISIBILITY_OPTIONS: { value: VisibilityLevel; label: string }[] = [
|
const VISIBILITY_OPTIONS: { value: VisibilityLevel; label: string; icon: string }[] = [
|
||||||
{ value: 'everyone', label: '所有人可见' },
|
{ value: 'everyone', label: '所有人可见', icon: 'earth' },
|
||||||
{ value: 'following', label: '仅关注的人可见' },
|
{ value: 'following', label: '仅关注的人可见', icon: 'account-group-outline' },
|
||||||
{ value: 'self', label: '仅自己可见' },
|
{ value: 'self', label: '仅自己可见', icon: 'lock-outline' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const PRIVACY_ITEMS: { key: keyof PrivacySettingsDTO; title: string; description: string }[] = [
|
const PRIVACY_ITEMS: { key: keyof PrivacySettingsDTO; title: string; description: string; icon: string }[] = [
|
||||||
{ key: 'followers_visibility', title: '粉丝列表', description: '谁可以查看你的粉丝列表' },
|
{ key: 'followers_visibility', title: '粉丝列表', description: '谁可以查看你的粉丝列表', icon: 'account-group' },
|
||||||
{ key: 'following_visibility', title: '关注列表', description: '谁可以查看你的关注列表' },
|
{ key: 'following_visibility', title: '关注列表', description: '谁可以查看你的关注列表', icon: 'account-heart' },
|
||||||
{ key: 'posts_visibility', title: '帖子列表', description: '谁可以查看你的帖子列表' },
|
{ key: 'posts_visibility', title: '帖子列表', description: '谁可以查看你的帖子列表', icon: 'text-box-outline' },
|
||||||
{ key: 'favorites_visibility', title: '收藏列表', description: '谁可以查看你的收藏列表' },
|
{ key: 'favorites_visibility', title: '收藏列表', description: '谁可以查看你的收藏列表', icon: 'star-outline' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRIVACY_NAV_ITEMS: { key: string; title: string; description: string; icon: string; danger?: boolean }[] = [
|
||||||
|
{ key: 'blocked_users', title: '黑名单', description: '管理你屏蔽的用户', icon: 'account-off-outline' },
|
||||||
|
{ key: 'account_deletion', title: '注销账号', description: '永久删除你的账号', icon: 'account-remove-outline', danger: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
function createPrivacySettingsStyles(colors: AppColors) {
|
function createPrivacySettingsStyles(colors: AppColors) {
|
||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.default,
|
||||||
},
|
},
|
||||||
loadingWrap: {
|
loadingWrap: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -44,56 +56,68 @@ function createPrivacySettingsStyles(colors: AppColors) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
paddingVertical: spacing.lg,
|
paddingVertical: spacing.md,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
maxWidth: CONTENT_MAX_WIDTH,
|
maxWidth: CONTENT_MAX_WIDTH,
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
section: {
|
groupHeader: {
|
||||||
marginBottom: spacing['2xl'],
|
paddingHorizontal: spacing.lg,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
},
|
},
|
||||||
sectionHeader: {
|
groupTitle: {
|
||||||
paddingHorizontal: spacing['2xl'],
|
|
||||||
marginBottom: spacing.sm,
|
|
||||||
marginTop: spacing.sm,
|
|
||||||
},
|
|
||||||
sectionTitle: {
|
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
fontSize: fontSizes.sm,
|
fontSize: fontSizes.sm,
|
||||||
color: colors.text.secondary,
|
color: colors.text.secondary,
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
},
|
},
|
||||||
// 扁平式布局:整行横向排列,底部边框分隔
|
groupContainer: {
|
||||||
|
backgroundColor: colors.background.paper,
|
||||||
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderColor: colors.divider,
|
||||||
|
overflow: 'visible',
|
||||||
|
},
|
||||||
item: {
|
item: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingVertical: spacing.lg,
|
paddingVertical: 14,
|
||||||
marginHorizontal: spacing['2xl'],
|
paddingHorizontal: spacing.lg,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: colors.divider,
|
borderBottomColor: colors.divider,
|
||||||
|
backgroundColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
itemLast: {
|
itemLast: {
|
||||||
borderBottomWidth: 0,
|
borderBottomWidth: 0,
|
||||||
},
|
},
|
||||||
itemTextWrap: {
|
itemLeft: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
backgroundColor: colors.background.default,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
marginRight: spacing.md,
|
marginRight: spacing.md,
|
||||||
},
|
},
|
||||||
|
dangerIconContainer: {
|
||||||
|
backgroundColor: colors.error.light + '20',
|
||||||
|
},
|
||||||
|
itemContent: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
itemTitle: {
|
itemTitle: {
|
||||||
fontWeight: '600',
|
fontWeight: '500',
|
||||||
fontSize: fontSizes.md,
|
|
||||||
color: colors.text.primary,
|
|
||||||
marginBottom: spacing.xs,
|
|
||||||
},
|
},
|
||||||
itemDescription: {
|
itemDescription: {
|
||||||
fontSize: fontSizes.sm,
|
marginTop: 1,
|
||||||
color: colors.text.secondary,
|
|
||||||
},
|
},
|
||||||
// 无背景色的选择框,紧贴右侧
|
|
||||||
selectBox: {
|
selectBox: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -104,10 +128,9 @@ function createPrivacySettingsStyles(colors: AppColors) {
|
|||||||
color: colors.text.secondary,
|
color: colors.text.secondary,
|
||||||
marginRight: spacing.xs,
|
marginRight: spacing.xs,
|
||||||
},
|
},
|
||||||
// 原位展开的下拉菜单
|
|
||||||
dropdownMenu: {
|
dropdownMenu: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: 0,
|
right: spacing.lg,
|
||||||
top: '100%',
|
top: '100%',
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
@@ -120,13 +143,17 @@ function createPrivacySettingsStyles(colors: AppColors) {
|
|||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
minWidth: 160,
|
minWidth: 180,
|
||||||
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
dropdownOption: {
|
dropdownOption: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
paddingHorizontal: spacing.md,
|
paddingHorizontal: spacing.md,
|
||||||
paddingVertical: spacing.sm,
|
paddingVertical: spacing.md,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: colors.divider,
|
borderBottomColor: colors.divider,
|
||||||
|
gap: spacing.sm,
|
||||||
},
|
},
|
||||||
dropdownOptionLast: {
|
dropdownOptionLast: {
|
||||||
borderBottomWidth: 0,
|
borderBottomWidth: 0,
|
||||||
@@ -139,6 +166,22 @@ function createPrivacySettingsStyles(colors: AppColors) {
|
|||||||
color: colors.primary.main,
|
color: colors.primary.main,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
dropdownOptionIcon: {
|
||||||
|
width: 22,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
navItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderBottomColor: colors.divider,
|
||||||
|
backgroundColor: colors.background.paper,
|
||||||
|
},
|
||||||
|
navItemLast: {
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +195,7 @@ export const PrivacySettingsScreen: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [openPicker, setOpenPicker] = useState<keyof PrivacySettingsDTO | null>(null);
|
const [openPicker, setOpenPicker] = useState<keyof PrivacySettingsDTO | null>(null);
|
||||||
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
|
const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 });
|
||||||
|
|
||||||
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
|
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
|
||||||
|
|
||||||
@@ -216,42 +259,52 @@ export const PrivacySettingsScreen: React.FC = () => {
|
|||||||
<SimpleHeader title="隐私设置" onBack={() => router.back()} />
|
<SimpleHeader title="隐私设置" onBack={() => router.back()} />
|
||||||
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
|
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<View style={styles.section}>
|
{/* 可见性设置分组 */}
|
||||||
{PRIVACY_ITEMS.map((item, index) => {
|
<View style={styles.groupHeader}>
|
||||||
const isOpen = openPicker === item.key;
|
<Text variant="caption" style={styles.groupTitle}>
|
||||||
return (
|
可见性设置
|
||||||
<View
|
|
||||||
key={item.key}
|
|
||||||
style={[
|
|
||||||
styles.item,
|
|
||||||
index === PRIVACY_ITEMS.length - 1 && styles.itemLast,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View style={styles.itemTextWrap}>
|
|
||||||
<Text variant="body" style={styles.itemTitle}>
|
|
||||||
{item.title}
|
|
||||||
</Text>
|
|
||||||
<Text variant="caption" color={colors.text.secondary} style={styles.itemDescription}>
|
|
||||||
{item.description}
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View style={[styles.groupContainer, { overflow: 'visible', zIndex: openPicker ? 10 : 1 }]}>
|
||||||
|
{PRIVACY_ITEMS.map((item, index) => {
|
||||||
|
const isOpen = openPicker === item.key;
|
||||||
|
const isLast = index === PRIVACY_ITEMS.length - 1;
|
||||||
|
return (
|
||||||
|
<View key={item.key} style={{ overflow: 'visible', zIndex: isOpen ? 10 : 1 }}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.selectBox}
|
style={[styles.item, !isOpen && isLast && styles.itemLast]}
|
||||||
onPress={() => setOpenPicker(isOpen ? null : item.key)}
|
onPress={() => setOpenPicker(isOpen ? null : item.key)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
|
<View style={styles.itemLeft}>
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={item.icon as any}
|
||||||
|
size={18}
|
||||||
|
color={colors.text.secondary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.itemContent}>
|
||||||
|
<Text variant="body" style={styles.itemTitle}>
|
||||||
|
{item.title}
|
||||||
|
</Text>
|
||||||
|
<Text variant="caption" color={colors.text.hint} style={styles.itemDescription}>
|
||||||
|
{item.description}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.selectBox}>
|
||||||
<Text style={styles.selectBoxText}>
|
<Text style={styles.selectBoxText}>
|
||||||
{getLabel(settings?.[item.key])}
|
{getLabel(settings?.[item.key])}
|
||||||
</Text>
|
</Text>
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={isOpen ? 'chevron-up' : 'chevron-down'}
|
name={isOpen ? 'chevron-up' : 'chevron-down'}
|
||||||
size={18}
|
size={16}
|
||||||
color={colors.text.secondary}
|
color={colors.text.secondary}
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* 原位展开的下拉选项 */}
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<View style={styles.dropdownMenu}>
|
<View style={styles.dropdownMenu}>
|
||||||
{VISIBILITY_OPTIONS.map((option, optIndex) => {
|
{VISIBILITY_OPTIONS.map((option, optIndex) => {
|
||||||
@@ -269,11 +322,18 @@ export const PrivacySettingsScreen: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
|
<View style={styles.dropdownOptionIcon}>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={option.icon as any}
|
||||||
|
size={16}
|
||||||
|
color={isActive ? colors.primary.main : colors.text.secondary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
<Text
|
<Text
|
||||||
variant="body"
|
variant="body"
|
||||||
style={[
|
style={[
|
||||||
styles.dropdownOptionText,
|
styles.dropdownOptionText,
|
||||||
isActive ? styles.dropdownOptionTextActive : undefined,
|
isActive && styles.dropdownOptionTextActive,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
@@ -287,6 +347,49 @@ export const PrivacySettingsScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 账号管理分组 */}
|
||||||
|
<View style={[styles.groupHeader, { marginTop: spacing.xl }]}>
|
||||||
|
<Text variant="caption" style={styles.groupTitle}>
|
||||||
|
账号管理
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.groupContainer}>
|
||||||
|
{PRIVACY_NAV_ITEMS.map((item, index) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={item.key}
|
||||||
|
style={[styles.navItem, index === PRIVACY_NAV_ITEMS.length - 1 && styles.navItemLast]}
|
||||||
|
onPress={() => {
|
||||||
|
if (item.key === 'blocked_users') router.push(hrefs.hrefProfileBlocked());
|
||||||
|
else if (item.key === 'account_deletion') router.push(hrefs.hrefProfileDeletion());
|
||||||
|
}}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View style={styles.itemLeft}>
|
||||||
|
<View style={[styles.iconContainer, item.danger && styles.dangerIconContainer]}>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={item.icon as any}
|
||||||
|
size={18}
|
||||||
|
color={item.danger ? colors.error.main : colors.text.secondary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.itemContent}>
|
||||||
|
<Text
|
||||||
|
variant="body"
|
||||||
|
color={item.danger ? colors.error.main : colors.text.primary}
|
||||||
|
style={styles.itemTitle}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</Text>
|
||||||
|
<Text variant="caption" color={colors.text.hint} style={styles.itemDescription}>
|
||||||
|
{item.description}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 采用扁平化设计,全宽布局,简洁分割线
|
* 采用扁平化设计,全宽布局,简洁分割线
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -34,6 +34,12 @@ import * as hrefs from '../../navigation/hrefs';
|
|||||||
|
|
||||||
const APP_VERSION = Constants.expoConfig?.version || '1.0.0';
|
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 {
|
interface SettingsItem {
|
||||||
key: string;
|
key: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -42,6 +48,7 @@ interface SettingsItem {
|
|||||||
showArrow?: boolean;
|
showArrow?: boolean;
|
||||||
danger?: boolean;
|
danger?: boolean;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
|
isThemePicker?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SETTINGS_GROUPS_BASE = [
|
const SETTINGS_GROUPS_BASE = [
|
||||||
@@ -52,8 +59,6 @@ const SETTINGS_GROUPS_BASE = [
|
|||||||
{ key: 'verification', title: '身份认证', icon: 'check-decagram', showArrow: true, subtitle: '获得认证标识,解锁更多功能' },
|
{ key: 'verification', title: '身份认证', icon: 'check-decagram', showArrow: true, subtitle: '获得认证标识,解锁更多功能' },
|
||||||
{ key: 'privacy', title: '隐私设置', icon: 'shield-account-outline', showArrow: true },
|
{ key: 'privacy', title: '隐私设置', icon: 'shield-account-outline', showArrow: true },
|
||||||
{ key: 'security', title: '账号安全', icon: 'lock-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,
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderColor: colors.divider,
|
borderColor: colors.divider,
|
||||||
|
overflow: 'visible',
|
||||||
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
groupHeader: {
|
groupHeader: {
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
@@ -134,6 +141,53 @@ function createSettingsStyles(colors: AppColors) {
|
|||||||
subtitle: {
|
subtitle: {
|
||||||
marginTop: 2,
|
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: {
|
logoutButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -172,12 +226,11 @@ export const SettingsScreen: React.FC = () => {
|
|||||||
const styles = useMemo(() => createSettingsStyles(colors), [colors]);
|
const styles = useMemo(() => createSettingsStyles(colors), [colors]);
|
||||||
const themePreference = useThemePreference();
|
const themePreference = useThemePreference();
|
||||||
const setThemePreference = useSetThemePreference();
|
const setThemePreference = useSetThemePreference();
|
||||||
|
const [openPicker, setOpenPicker] = useState<string | null>(null);
|
||||||
|
|
||||||
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
|
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
|
||||||
|
|
||||||
const settingsGroups = useMemo(() => {
|
const settingsGroups = useMemo(() => {
|
||||||
const themeSubtitle =
|
|
||||||
themePreference === 'system' ? '跟随系统' : themePreference === 'dark' ? '深色' : '浅色';
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: '个性化',
|
title: '个性化',
|
||||||
@@ -193,8 +246,7 @@ export const SettingsScreen: React.FC = () => {
|
|||||||
key: 'theme',
|
key: 'theme',
|
||||||
title: '主题模式',
|
title: '主题模式',
|
||||||
icon: 'theme-light-dark',
|
icon: 'theme-light-dark',
|
||||||
showArrow: true,
|
isThemePicker: true,
|
||||||
subtitle: themeSubtitle,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -208,31 +260,7 @@ export const SettingsScreen: React.FC = () => {
|
|||||||
router.push(hrefs.hrefProfileChatSettings());
|
router.push(hrefs.hrefProfileChatSettings());
|
||||||
break;
|
break;
|
||||||
case 'theme': {
|
case 'theme': {
|
||||||
Alert.alert(
|
setOpenPicker(openPicker === 'theme' ? null : 'theme');
|
||||||
'主题模式',
|
|
||||||
'选择应用界面明暗',
|
|
||||||
[
|
|
||||||
{ text: '取消', style: 'cancel' },
|
|
||||||
{
|
|
||||||
text: '跟随系统',
|
|
||||||
onPress: () => {
|
|
||||||
void setThemePreference('system');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '浅色',
|
|
||||||
onPress: () => {
|
|
||||||
void setThemePreference('light');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '深色',
|
|
||||||
onPress: () => {
|
|
||||||
void setThemePreference('dark');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
break;
|
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
|
<TouchableOpacity
|
||||||
key={item.key}
|
key={item.key}
|
||||||
style={[
|
style={[
|
||||||
styles.settingItem,
|
styles.settingItem,
|
||||||
index === total - 1 && styles.settingItemLast,
|
index === total - 1 && !isThemeOpen && styles.settingItemLast,
|
||||||
]}
|
]}
|
||||||
onPress={() => (item.onPress ? item.onPress() : handleItemPress(item.key))}
|
onPress={() => (item.onPress ? item.onPress() : handleItemPress(item.key))}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
@@ -318,14 +353,64 @@ export const SettingsScreen: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{item.showArrow && (
|
{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} />
|
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderGroup = (group: (typeof settingsGroups)[0]) => (
|
const renderGroup = (group: (typeof settingsGroups)[0], groupIndex: number) => (
|
||||||
<View key={group.title}>
|
<View
|
||||||
|
key={group.title}
|
||||||
|
style={[
|
||||||
|
{ overflow: 'visible', zIndex: group.items.some((i) => i.isThemePicker && openPicker === 'theme') ? 10 : groupIndex },
|
||||||
|
]}
|
||||||
|
>
|
||||||
<View style={styles.groupHeader}>
|
<View style={styles.groupHeader}>
|
||||||
<Text variant="caption" style={styles.groupTitle}>
|
<Text variant="caption" style={styles.groupTitle}>
|
||||||
{group.title}
|
{group.title}
|
||||||
@@ -352,7 +437,7 @@ export const SettingsScreen: React.FC = () => {
|
|||||||
|
|
||||||
const renderContent = () => (
|
const renderContent = () => (
|
||||||
<>
|
<>
|
||||||
{settingsGroups.map((group) => renderGroup(group))}
|
{settingsGroups.map((group, index) => renderGroup(group, index))}
|
||||||
{renderLogoutButton()}
|
{renderLogoutButton()}
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<Text variant="caption" color={colors.text.hint}>
|
<Text variant="caption" color={colors.text.hint}>
|
||||||
|
|||||||
Reference in New Issue
Block a user