feat(profile): add chat settings and language options to profile settings
All checks were successful
Frontend CI / ota-android (push) Successful in 13m7s
Frontend CI / build-and-push-web (push) Successful in 16m58s
Frontend CI / build-android-apk (push) Successful in 1h4m18s

- Introduced hrefProfileChatSettings() for navigation to chat settings.
- Updated SettingsScreen to include new settings items for chat settings, language selection, data storage, terms, and privacy policy.
- Enhanced user experience by providing alerts for language and data storage options.

This update improves the profile settings interface by adding more customization options for users.
This commit is contained in:
lafay
2026-04-01 02:17:36 +08:00
parent 20e9d69540
commit 5614b4078a
25 changed files with 2622 additions and 801 deletions

View File

@@ -0,0 +1,499 @@
/**
* 聊天设置页 ChatSettingsScreen
* 胡萝卜BBS - 聊天个性化设置
*/
import React, { useMemo, useState, useCallback } from 'react';
import {
View,
StyleSheet,
TouchableOpacity,
ScrollView,
Dimensions,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import {
useAppColors,
spacing,
fontSizes,
borderRadius,
type AppColors,
} from '../../theme';
import { Text } from '../../components/common';
import { useResponsiveSpacing } from '../../hooks';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const CARD_MAX_WIDTH = 720;
// 主题配置
const THEMES = [
{ id: 'default', name: '默认绿', primary: '#4CAF50', secondary: '#E8F5E9', bubble: '#DCF8C6', icon: '🏠' },
{ id: 'yellow', name: '活力黄', primary: '#FFC107', secondary: '#FFF8E1', bubble: '#FFECB3', icon: '🐥' },
{ id: 'blue', name: '清新蓝', primary: '#2196F3', secondary: '#E3F2FD', bubble: '#BBDEFB', icon: '☃️' },
{ id: 'purple', name: '梦幻紫', primary: '#9C27B0', secondary: '#F3E5F5', bubble: '#E1BEE7', icon: '💎' },
{ id: 'teal', name: '自然青', primary: '#009688', secondary: '#E0F2F1', bubble: '#B2DFDB', icon: '🦁' },
] as const;
interface SliderProps {
value: number;
min: number;
max: number;
onValueChange: (value: number) => void;
leftLabel?: string;
rightLabel?: string;
showValue?: boolean;
}
// 自定义滑块组件
const Slider: React.FC<SliderProps> = ({
value,
min,
max,
onValueChange,
leftLabel,
rightLabel,
showValue = true,
}) => {
const colors = useAppColors();
const styles = useMemo(() => createSliderStyles(colors), [colors]);
const percentage = ((value - min) / (max - min)) * 100;
const handlePress = useCallback((event: any) => {
const { locationX } = event.nativeEvent;
const sliderWidth = SCREEN_WIDTH - 64; // 考虑边距
const newPercentage = Math.max(0, Math.min(100, (locationX / sliderWidth) * 100));
const newValue = Math.round(min + (newPercentage / 100) * (max - min));
onValueChange(newValue);
}, [min, max, onValueChange]);
return (
<View style={styles.container}>
<View style={styles.labelRow}>
{leftLabel && <Text style={styles.label}>{leftLabel}</Text>}
{showValue && <Text style={styles.value}>{value}</Text>}
{rightLabel && <Text style={styles.label}>{rightLabel}</Text>}
</View>
<TouchableOpacity
style={styles.trackContainer}
onPress={handlePress}
activeOpacity={1}
>
<View style={styles.track}>
<View style={[styles.fill, { width: `${percentage}%`, backgroundColor: colors.primary.main }]} />
</View>
<View style={[styles.thumb, { left: `${percentage}%`, backgroundColor: colors.primary.main }]} />
</TouchableOpacity>
</View>
);
};
function createSliderStyles(colors: AppColors) {
return StyleSheet.create({
container: {
marginVertical: spacing.sm,
},
labelRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.xs,
},
label: {
fontSize: fontSizes.sm,
color: colors.text.secondary,
},
value: {
fontSize: fontSizes.md,
fontWeight: '600',
color: colors.primary.main,
},
trackContainer: {
height: 24,
justifyContent: 'center',
},
track: {
height: 4,
backgroundColor: colors.divider,
borderRadius: 2,
},
fill: {
height: '100%',
borderRadius: 2,
},
thumb: {
position: 'absolute',
width: 20,
height: 20,
borderRadius: 10,
marginLeft: -10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 2,
elevation: 3,
},
});
}
function createStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.default,
},
scrollContent: {
paddingVertical: spacing.md,
},
section: {
marginBottom: spacing.lg,
maxWidth: CARD_MAX_WIDTH,
alignSelf: 'center',
width: '100%',
paddingHorizontal: spacing.lg,
},
sectionTitle: {
fontSize: fontSizes.sm,
fontWeight: '600',
color: colors.text.secondary,
marginBottom: spacing.sm,
marginLeft: spacing.xs,
},
card: {
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
padding: spacing.md,
},
// 聊天预览样式
previewContainer: {
backgroundColor: '#E8F5E9',
borderRadius: borderRadius.lg,
padding: spacing.md,
minHeight: 140,
},
previewMessage: {
backgroundColor: '#FFFFFF',
borderRadius: borderRadius.md,
padding: spacing.sm,
marginBottom: spacing.sm,
maxWidth: '80%',
alignSelf: 'flex-start',
},
previewReply: {
borderRadius: borderRadius.md,
padding: spacing.sm,
maxWidth: '80%',
alignSelf: 'flex-end',
},
previewText: {
fontSize: fontSizes.sm,
color: '#333',
},
previewTime: {
fontSize: fontSizes.xs,
color: '#999',
marginTop: 2,
alignSelf: 'flex-end',
},
// 主题选择样式
themeList: {
flexDirection: 'row',
gap: spacing.sm,
},
themeItem: {
width: 70,
height: 90,
borderRadius: borderRadius.md,
borderWidth: 2,
borderColor: 'transparent',
overflow: 'hidden',
padding: spacing.xs,
},
themeItemActive: {
borderColor: colors.primary.main,
},
themePreview: {
flex: 1,
justifyContent: 'center',
gap: 4,
},
themeBubble1: {
height: 12,
borderRadius: 6,
width: '70%',
},
themeBubble2: {
height: 12,
borderRadius: 6,
width: '50%',
alignSelf: 'flex-end',
},
themeIcon: {
position: 'absolute',
bottom: 4,
right: 4,
width: 18,
height: 18,
borderRadius: 9,
justifyContent: 'center',
alignItems: 'center',
},
themeName: {
fontSize: fontSizes.xs,
textAlign: 'center',
marginTop: 4,
color: colors.text.secondary,
},
// 设置项样式
settingItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: spacing.md,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
},
settingItemLast: {
borderBottomWidth: 0,
},
settingItemLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.md,
},
iconContainer: {
width: 36,
height: 36,
borderRadius: borderRadius.md,
backgroundColor: colors.primary.light + '20',
justifyContent: 'center',
alignItems: 'center',
},
settingTitle: {
fontSize: fontSizes.md,
color: colors.text.primary,
},
settingSubtitle: {
fontSize: fontSizes.sm,
color: colors.text.secondary,
marginTop: 2,
},
// 开关样式
switch: {
width: 50,
height: 28,
borderRadius: 14,
backgroundColor: colors.divider,
padding: 2,
},
switchActive: {
backgroundColor: colors.primary.main,
},
switchThumb: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: '#fff',
},
});
}
export const ChatSettingsScreen: React.FC = () => {
const router = useRouter();
const colors = useAppColors();
const styles = useMemo(() => createStyles(colors), [colors]);
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
// 状态
const [fontSize, setFontSize] = useState(16);
const [messageRadius, setMessageRadius] = useState(17);
const [selectedTheme, setSelectedTheme] = useState(0);
const [nightMode, setNightMode] = useState(false);
const currentTheme = THEMES[selectedTheme];
// 渲染聊天预览
const renderChatPreview = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={[styles.previewContainer, { backgroundColor: currentTheme.secondary }]}>
<View style={[styles.previewMessage, { borderRadius: messageRadius }]}>
<Text style={[styles.previewText, { fontSize }]}>MiMQy</Text>
<Text style={[styles.previewText, { fontSize: fontSize - 2 }]}>
👋
</Text>
<Text style={[styles.previewText, { fontSize: fontSize - 2 }]}>
</Text>
<Text style={styles.previewTime}>AM12:42</Text>
</View>
<View style={[styles.previewReply, { borderRadius: messageRadius, backgroundColor: currentTheme.bubble }]}>
<Text style={[styles.previewText, { fontSize }]}>
😎
</Text>
<Text style={styles.previewTime}>AM12:57 </Text>
</View>
</View>
</View>
);
// 渲染字号滑块
const renderFontSizeSlider = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.card}>
<Slider
value={fontSize}
min={12}
max={24}
onValueChange={setFontSize}
leftLabel="A"
rightLabel="A"
/>
</View>
</View>
);
// 渲染主题颜色选择
const renderThemeColors = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.themeList}
>
{THEMES.map((theme, index) => (
<TouchableOpacity
key={theme.id}
style={[
styles.themeItem,
selectedTheme === index && styles.themeItemActive,
{ backgroundColor: theme.secondary },
]}
onPress={() => setSelectedTheme(index)}
activeOpacity={0.8}
>
<View style={styles.themePreview}>
<View style={[styles.themeBubble1, { backgroundColor: '#FFFFFF' }]} />
<View style={[styles.themeBubble2, { backgroundColor: theme.bubble }]} />
</View>
<View style={[styles.themeIcon, { backgroundColor: theme.primary }]}>
<Text style={{ fontSize: 10 }}>{theme.icon}</Text>
</View>
</TouchableOpacity>
))}
</ScrollView>
</View>
);
// 渲染圆角滑块
const renderRadiusSlider = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.card}>
<Slider
value={messageRadius}
min={0}
max={30}
onValueChange={setMessageRadius}
/>
</View>
</View>
);
// 渲染其他设置项
const renderOtherSettings = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.card}>
<TouchableOpacity style={styles.settingItem}>
<View style={styles.settingItemLeft}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons name="image-outline" size={20} color={colors.primary.main} />
</View>
<View>
<Text style={styles.settingTitle}></Text>
</View>
</View>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
</TouchableOpacity>
<TouchableOpacity style={[styles.settingItem, styles.settingItemLast]}>
<View style={styles.settingItemLeft}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons name="palette-outline" size={20} color={colors.primary.main} />
</View>
<View>
<Text style={styles.settingTitle}></Text>
<Text style={styles.settingSubtitle}>MiMQy</Text>
</View>
</View>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
</TouchableOpacity>
</View>
</View>
);
// 渲染夜间模式开关
const renderNightMode = () => (
<View style={styles.section}>
<View style={styles.card}>
<View style={[styles.settingItem, styles.settingItemLast]}>
<View style={styles.settingItemLeft}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons name="weather-night" size={20} color={colors.primary.main} />
</View>
<Text style={styles.settingTitle}></Text>
</View>
<TouchableOpacity
style={[styles.switch, nightMode && styles.switchActive]}
onPress={() => setNightMode(!nightMode)}
>
<View style={[styles.switchThumb, { alignSelf: nightMode ? 'flex-end' : 'flex-start' }]} />
</TouchableOpacity>
</View>
</View>
</View>
);
// 渲染浏览其他主题
const renderBrowseThemes = () => (
<View style={styles.section}>
<TouchableOpacity style={styles.card}>
<View style={[styles.settingItem, styles.settingItemLast]}>
<View style={styles.settingItemLeft}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons name="apps" size={20} color={colors.primary.main} />
</View>
<Text style={styles.settingTitle}></Text>
</View>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
</View>
</TouchableOpacity>
</View>
);
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView
contentContainerStyle={[
styles.scrollContent,
{ paddingHorizontal: responsivePadding }
]}
>
{renderFontSizeSlider()}
{renderChatPreview()}
{renderThemeColors()}
{renderRadiusSlider()}
{renderOtherSettings()}
{renderNightMode()}
{renderBrowseThemes()}
</ScrollView>
</SafeAreaView>
);
};
export default ChatSettingsScreen;