feat(Theme): enhance theming and UI consistency across components
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 8m15s
Frontend CI / ota-android (push) Successful in 10m56s
Frontend CI / build-android-apk (push) Successful in 1h3m22s

- Updated app.json to set userInterfaceStyle to automatic for improved theme adaptability.
- Refactored layout components to utilize useAppColors for dynamic theming, ensuring consistent color usage.
- Introduced SystemChrome component to manage system UI background color based on theme.
- Enhanced TabsLayout, ProfileStackLayout, and other components to leverage new theming structure.
- Improved QRCodeScanner, SearchBar, and CommentItem styles to align with the updated theme system.
- Consolidated styles in SystemMessageItem and TabBar for better maintainability and visual coherence.
This commit is contained in:
lafay
2026-03-25 05:16:54 +08:00
parent 90d834695f
commit 4ee3079b9f
86 changed files with 6777 additions and 5890 deletions

View File

@@ -4,7 +4,7 @@
* 在宽屏下居中显示,最大宽度限制
*/
import React from 'react';
import React, { useMemo } from 'react';
import {
View,
StyleSheet,
@@ -16,7 +16,15 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import Constants from 'expo-constants';
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
import {
useAppColors,
spacing,
fontSizes,
borderRadius,
useThemePreference,
useSetThemePreference,
type AppColors,
} from '../../theme';
import { useAuthStore } from '../../stores';
import { Text, ResponsiveContainer } from '../../components/common';
import { useResponsive } from '../../hooks';
@@ -34,11 +42,9 @@ interface SettingsItem {
subtitle?: string;
}
// 获取应用版本号
const APP_VERSION = Constants.expoConfig?.version || '1.0.0';
// 设置分组配置
const SETTINGS_GROUPS = [
const SETTINGS_GROUPS_BASE = [
{
title: '账号与安全',
icon: 'shield-check-outline',
@@ -66,19 +72,161 @@ const SETTINGS_GROUPS = [
},
];
function createSettingsStyles(colors: AppColors) {
return 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 const SettingsScreen: React.FC = () => {
const router = useRouter();
const { logout } = useAuthStore();
const { isWideScreen } = useResponsive();
const insets = useSafeAreaInsets();
const { isMobile } = useResponsive();
// 底部间距,避免被 TabBar 遮挡
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: '显示与外观',
icon: 'palette-outline',
items: [
{
key: 'theme',
title: '主题模式',
icon: 'theme-light-dark',
showArrow: true,
subtitle: themeSubtitle,
},
],
},
...SETTINGS_GROUPS_BASE,
];
}, [themePreference]);
const handleItemPress = (key: string) => {
switch (key) {
case 'theme':
Alert.alert('主题模式', '选择应用界面明暗', [
{ text: '取消', style: 'cancel' },
{
text: '跟随系统',
onPress: () => {
void setThemePreference('system');
},
},
{
text: '浅色',
onPress: () => {
void setThemePreference('light');
},
},
{
text: '深色',
onPress: () => {
void setThemePreference('dark');
},
},
]);
break;
case 'edit_profile':
router.push(hrefs.hrefProfileEdit());
break;
@@ -103,7 +251,7 @@ export const SettingsScreen: React.FC = () => {
onPress: async () => {
await logout();
router.replace(hrefs.hrefAuthLogin());
}
},
},
]
);
@@ -113,7 +261,6 @@ export const SettingsScreen: React.FC = () => {
}
};
// 渲染单个设置项
const renderSettingItem = (item: SettingsItem, index: number, total: number) => (
<TouchableOpacity
key={item.key}
@@ -122,14 +269,11 @@ export const SettingsScreen: React.FC = () => {
index === 0 && styles.settingItemFirst,
index === total - 1 && styles.settingItemLast,
]}
onPress={() => item.onPress ? item.onPress() : handleItemPress(item.key)}
onPress={() => (item.onPress ? item.onPress() : handleItemPress(item.key))}
activeOpacity={0.7}
>
<View style={styles.settingItemLeft}>
<View style={[
styles.iconContainer,
item.danger && styles.dangerIconContainer
]}>
<View style={[styles.iconContainer, item.danger && styles.dangerIconContainer]}>
<MaterialCommunityIcons
name={item.icon as any}
size={20}
@@ -137,10 +281,7 @@ export const SettingsScreen: React.FC = () => {
/>
</View>
<View style={styles.settingContent}>
<Text
variant="body"
color={item.danger ? colors.error.main : colors.text.primary}
>
<Text variant="body" color={item.danger ? colors.error.main : colors.text.primary}>
{item.title}
</Text>
{item.subtitle && (
@@ -156,8 +297,7 @@ export const SettingsScreen: React.FC = () => {
</TouchableOpacity>
);
// 渲染分组
const renderGroup = (group: typeof SETTINGS_GROUPS[0], groupIndex: number) => (
const renderGroup = (group: (typeof settingsGroups)[0]) => (
<View key={group.title} style={styles.groupContainer}>
<View style={styles.groupHeader}>
<MaterialCommunityIcons name={group.icon as any} size={16} color={colors.primary.main} />
@@ -166,14 +306,11 @@ export const SettingsScreen: React.FC = () => {
</Text>
</View>
<View style={styles.card}>
{group.items.map((item, index) =>
renderSettingItem(item, index, group.items.length)
)}
{group.items.map((item, index) => renderSettingItem(item, index, group.items.length))}
</View>
</View>
);
// 退出登录按钮
const renderLogoutButton = () => (
<TouchableOpacity
style={styles.logoutButton}
@@ -187,13 +324,11 @@ export const SettingsScreen: React.FC = () => {
</TouchableOpacity>
);
// 渲染内容
const renderContent = () => (
<>
{SETTINGS_GROUPS.map((group, index) => renderGroup(group, index))}
{settingsGroups.map((group) => renderGroup(group))}
{renderLogoutButton()}
{/* 底部版权信息 */}
<View style={styles.footer}>
<Text variant="caption" color={colors.text.hint}>
v{APP_VERSION}
@@ -222,98 +357,4 @@ export const SettingsScreen: React.FC = () => {
);
};
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;