Files
digital-pilates/app/settings/tab-bar-config.tsx
richarjiang fbe0c92f0f feat(i18n): 全面实现应用核心功能模块的国际化支持
- 新增 i18n 翻译资源,覆盖睡眠、饮水、体重、锻炼、用药 AI 识别、步数、健身圆环、基础代谢及设置等核心模块
- 重构相关页面及组件(如 SleepDetail, WaterDetail, WorkoutHistory 等)使用 `useI18n` 钩子替换硬编码文本
- 升级 `utils/date` 工具库与 `DateSelector` 组件,支持基于语言环境的日期格式化与显示
- 完善登录页、注销流程及权限申请弹窗的双语提示信息
- 优化部分页面的 UI 细节与字体样式以适配多语言显示
2025-11-27 17:54:36 +08:00

264 lines
6.8 KiB
TypeScript

import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import {
resetToDefault,
selectTabBarConfigs,
toggleTabEnabled,
type TabConfig,
} from '@/store/tabBarConfigSlice';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import React, { useCallback } from 'react';
import {
Alert,
ScrollView,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View
} from 'react-native';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { IconSymbol } from '@/components/ui/IconSymbol';
import { palette } from '@/constants/Colors';
import { useI18n } from '@/hooks/useI18n';
export default function TabBarConfigScreen() {
const { t } = useI18n();
const router = useRouter();
const dispatch = useAppDispatch();
const safeAreaTop = useSafeAreaTop(60);
const configs = useAppSelector(selectTabBarConfigs);
// 处理开关切换
const handleToggle = useCallback(
(tabId: string) => {
dispatch(toggleTabEnabled(tabId));
},
[dispatch]
);
// 恢复默认设置
const handleReset = useCallback(() => {
Alert.alert(
t('personal.tabBarConfig.resetConfirm.title'),
t('personal.tabBarConfig.resetConfirm.message'),
[
{
text: t('personal.tabBarConfig.resetConfirm.cancel'),
style: 'cancel',
},
{
text: t('personal.tabBarConfig.resetConfirm.confirm'),
style: 'destructive',
onPress: () => {
dispatch(resetToDefault());
Alert.alert('', t('personal.tabBarConfig.resetSuccess'));
},
},
]
);
}, [dispatch, t]);
// 渲染单个 Tab 行
const renderTabRow = useCallback(
(item: TabConfig, index: number, total: number) => {
return (
<View key={item.id}>
<View style={styles.tabItem}>
{/* Tab 图标和名称 */}
<View style={styles.tabInfo}>
<View style={styles.iconContainer}>
<IconSymbol name={item.icon as any} size={24} color="#9370DB" />
</View>
<View style={styles.tabTextContainer}>
<Text style={styles.tabTitle}>{t(item.titleKey)}</Text>
{!item.canBeDisabled && (
<Text style={styles.tabSubtitle}>
{t('personal.tabBarConfig.cannotDisable')}
</Text>
)}
</View>
</View>
{/* 开关 */}
<Switch
value={item.enabled}
onValueChange={() => handleToggle(item.id)}
disabled={!item.canBeDisabled}
trackColor={{ false: '#E5E5E5', true: '#9370DB' }}
thumbColor="#FFFFFF"
style={styles.switch}
/>
</View>
{/* 分割线 - 最后一项不显示 */}
{index < total - 1 && (
<View style={styles.separatorContainer}>
<View style={styles.separator} />
</View>
)}
</View>
);
},
[handleToggle, t]
);
return (
<View style={styles.container}>
<LinearGradient
colors={[palette.purple[100], '#F5F5F5']}
start={{ x: 1, y: 0 }}
end={{ x: 0.3, y: 0.4 }}
style={styles.gradientBackground}
/>
{/* 顶部导航栏 */}
<HeaderBar
title={t('personal.tabBarConfig.title')}
onBack={() => router.back()}
right={
<TouchableOpacity onPress={handleReset} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
<Text style={styles.headerRightButton}>
{t('personal.tabBarConfig.resetButton')}
</Text>
</TouchableOpacity>
}
/>
{/* 主内容区 */}
<ScrollView
style={styles.content}
contentContainerStyle={[styles.scrollContent, { paddingTop: safeAreaTop }]} // 增加顶部间距,因为 HeaderBar 现在是 absolute 的
showsVerticalScrollIndicator={false}
>
{/* 说明区域 */}
<View style={styles.headerSection}>
<Text style={styles.subtitle}>{t('personal.tabBarConfig.subtitle')}</Text>
<View style={styles.descriptionCard}>
<View style={styles.hintRow}>
<Ionicons name="information-circle-outline" size={20} color="#9370DB" />
<Text style={styles.descriptionText}>
{t('personal.tabBarConfig.description')}
</Text>
</View>
</View>
</View>
{/* Tab 列表 - 聚合在一个卡片中 */}
<View style={styles.sectionContainer}>
{configs.map((item, index) => renderTabRow(item, index, configs.length))}
</View>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
height: '60%', // 渐变覆盖上半部分即可
},
content: {
flex: 1,
},
scrollContent: {
paddingHorizontal: 16,
paddingBottom: 40,
},
headerSection: {
marginBottom: 16,
},
subtitle: {
fontSize: 14,
color: '#6C757D',
marginBottom: 12,
},
descriptionCard: {
backgroundColor: 'rgba(255, 255, 255, 0.6)',
borderRadius: 12,
padding: 12,
gap: 8,
borderWidth: 1,
borderColor: 'rgba(147, 112, 219, 0.1)',
},
hintRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
descriptionText: {
flex: 1,
fontSize: 13,
color: '#2C3E50',
lineHeight: 18,
},
sectionContainer: {
backgroundColor: '#FFFFFF',
borderRadius: 20,
marginBottom: 20,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.03,
shadowRadius: 8,
elevation: 2,
},
tabItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
paddingVertical: 16,
},
separatorContainer: {
paddingLeft: 68, // 40(icon) + 12(gap) + 16(padding)
paddingRight: 16,
},
separator: {
height: 1,
backgroundColor: '#F0F0F0',
},
tabInfo: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
iconContainer: {
width: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
},
tabTextContainer: {
flex: 1,
},
tabTitle: {
fontSize: 16,
fontWeight: '500',
color: '#2C3E50',
marginBottom: 2,
},
tabSubtitle: {
fontSize: 12,
color: '#9370DB',
},
switch: {
transform: [{ scaleX: 0.9 }, { scaleY: 0.9 }],
},
headerRightButton: {
fontSize: 15,
fontWeight: '600',
color: '#9370DB', // 使用主色调
},
});