- 新增底部标签栏配置页面,支持切换标签显示/隐藏和恢复默认设置 - 实现已服用药物的堆叠卡片展示,优化药物列表视觉层次 - 集成Redux状态管理底部标签栏配置,支持本地持久化 - 优化个人中心页面背景渐变效果,移除装饰性圆圈元素 - 更新启动页和应用图标为新的品牌视觉 - 药物详情页AI分析加载动画替换为Lottie动画 - 调整药物卡片圆角半径提升视觉一致性 - 新增多语言支持(中英文)用于标签栏配置界面 主要改进: 1. 用户可以自定义底部导航栏显示内容 2. 已完成的药物以堆叠形式展示,节省空间 3. 配置数据通过AsyncStorage持久化保存 4. 支持默认配置恢复功能
224 lines
6.8 KiB
TypeScript
224 lines
6.8 KiB
TypeScript
import type { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs';
|
|
import { GlassContainer, GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
|
import * as Haptics from 'expo-haptics';
|
|
import { Tabs, usePathname } from 'expo-router';
|
|
import { Icon, Label, NativeTabs } from 'expo-router/unstable-native-tabs';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import React from 'react';
|
|
import { Text, TouchableOpacity, View, ViewStyle } from 'react-native';
|
|
|
|
import { IconSymbol } from '@/components/ui/IconSymbol';
|
|
import { Colors } from '@/constants/Colors';
|
|
import { ROUTES } from '@/constants/Routes';
|
|
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
|
import { useAppSelector } from '@/hooks/redux';
|
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
import { selectEnabledTabs } from '@/store/tabBarConfigSlice';
|
|
|
|
// Tab configuration
|
|
type TabConfig = {
|
|
icon: string;
|
|
titleKey: string;
|
|
};
|
|
|
|
const TAB_CONFIGS: Record<string, TabConfig> = {
|
|
statistics: { icon: 'chart.pie.fill', titleKey: 'statistics.tabs.health' },
|
|
medications: { icon: 'pills.fill', titleKey: 'statistics.tabs.medications' },
|
|
fasting: { icon: 'timer', titleKey: 'statistics.tabs.fasting' },
|
|
challenges: { icon: 'trophy.fill', titleKey: 'statistics.tabs.challenges' },
|
|
personal: { icon: 'person.fill', titleKey: 'statistics.tabs.personal' },
|
|
};
|
|
|
|
export default function TabLayout() {
|
|
const { t } = useTranslation();
|
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
|
const colorTokens = Colors[theme];
|
|
const pathname = usePathname();
|
|
const glassEffectAvailable = isLiquidGlassAvailable();
|
|
|
|
// 获取已启用的标签配置(按自定义顺序)
|
|
const enabledTabs = useAppSelector(selectEnabledTabs);
|
|
|
|
// Helper function to determine if a tab is selected
|
|
const isTabSelected = (routeName: string): boolean => {
|
|
const routeMap: Record<string, string> = {
|
|
statistics: ROUTES.TAB_STATISTICS,
|
|
medications: ROUTES.TAB_MEDICATIONS,
|
|
fasting: ROUTES.TAB_FASTING,
|
|
challenges: ROUTES.TAB_CHALLENGES,
|
|
personal: ROUTES.TAB_PERSONAL,
|
|
};
|
|
|
|
return routeMap[routeName] === pathname || pathname.includes(routeName);
|
|
};
|
|
|
|
// Custom tab button component
|
|
const createTabButton = (routeName: string) => (props: any) => {
|
|
const { onPress } = props;
|
|
const tabConfig = TAB_CONFIGS[routeName];
|
|
|
|
if (!tabConfig) return null;
|
|
|
|
const isSelected = isTabSelected(routeName);
|
|
|
|
const handlePress = (event: any) => {
|
|
if (process.env.EXPO_OS === 'ios') {
|
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
}
|
|
onPress?.(event);
|
|
};
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
onPress={handlePress}
|
|
accessibilityRole="button"
|
|
activeOpacity={1}
|
|
style={{
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
flexDirection: 'row',
|
|
marginHorizontal: 2,
|
|
marginVertical: 10,
|
|
borderRadius: 25,
|
|
backgroundColor: isSelected ? colorTokens.tabBarActiveBackground : 'transparent',
|
|
paddingHorizontal: isSelected ? 8 : 4,
|
|
paddingVertical: 8,
|
|
}}
|
|
>
|
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
<IconSymbol
|
|
size={22}
|
|
name={tabConfig.icon as any}
|
|
color={isSelected ? colorTokens.tabIconSelected : colorTokens.tabIconDefault}
|
|
/>
|
|
{isSelected && (
|
|
<Text
|
|
style={{
|
|
color: colorTokens.tabIconSelected,
|
|
fontSize: 12,
|
|
fontWeight: '600',
|
|
marginLeft: 6,
|
|
}}
|
|
numberOfLines={1}
|
|
>
|
|
{t(tabConfig.titleKey)}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
// Custom tab bar background component
|
|
const TabBarBackground = () => {
|
|
if (glassEffectAvailable) {
|
|
return (
|
|
<GlassContainer
|
|
spacing={8}
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
borderRadius: 34,
|
|
}}
|
|
>
|
|
<GlassView
|
|
isInteractive
|
|
glassEffectStyle="regular"
|
|
tintColor={theme === 'dark' ? 'rgba(0,0,0,0.3)' : 'rgba(255,255,255,0.3)'}
|
|
style={{
|
|
flex: 1,
|
|
borderRadius: 34,
|
|
}}
|
|
/>
|
|
</GlassContainer>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// Common screen options
|
|
const getScreenOptions = (routeName: string): BottomTabNavigationOptions => ({
|
|
headerShown: false,
|
|
tabBarActiveTintColor: colorTokens.tabIconSelected,
|
|
tabBarButton: createTabButton(routeName),
|
|
tabBarBackground: TabBarBackground,
|
|
tabBarStyle: {
|
|
position: 'absolute',
|
|
bottom: TAB_BAR_BOTTOM_OFFSET,
|
|
height: TAB_BAR_HEIGHT,
|
|
borderRadius: 34,
|
|
backgroundColor: glassEffectAvailable ? 'transparent' : colorTokens.tabBarBackground,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: glassEffectAvailable ? 0.1 : 0.2,
|
|
shadowRadius: 10,
|
|
elevation: 5,
|
|
paddingHorizontal: 6,
|
|
paddingTop: 0,
|
|
paddingBottom: 0,
|
|
marginHorizontal: 16,
|
|
left: 16,
|
|
right: 16,
|
|
alignSelf: 'center',
|
|
borderWidth: glassEffectAvailable ? 1 : 0,
|
|
borderColor: glassEffectAvailable ? (theme === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)') : 'transparent',
|
|
} as ViewStyle,
|
|
tabBarItemStyle: {
|
|
backgroundColor: 'transparent',
|
|
height: TAB_BAR_HEIGHT,
|
|
marginTop: 0,
|
|
marginBottom: 0,
|
|
paddingTop: 0,
|
|
paddingBottom: 0,
|
|
},
|
|
tabBarShowLabel: false,
|
|
});
|
|
|
|
// 根据配置渲染标签页
|
|
if (glassEffectAvailable) {
|
|
return (
|
|
<NativeTabs>
|
|
{enabledTabs.map((tab) => {
|
|
const tabConfig = TAB_CONFIGS[tab.id];
|
|
if (!tabConfig) return null;
|
|
|
|
return (
|
|
<NativeTabs.Trigger key={tab.id} name={tab.id}>
|
|
<Icon sf={tabConfig.icon as any} drawable="custom_android_drawable" />
|
|
<Label>{t(tabConfig.titleKey)}</Label>
|
|
</NativeTabs.Trigger>
|
|
);
|
|
})}
|
|
</NativeTabs>
|
|
);
|
|
}
|
|
|
|
// 确定初始路由(第一个启用的标签)
|
|
const initialRouteName = enabledTabs.length > 0 ? enabledTabs[0].id : 'statistics';
|
|
|
|
return (
|
|
<Tabs
|
|
initialRouteName={initialRouteName}
|
|
screenOptions={({ route }) => getScreenOptions(route.name)}
|
|
>
|
|
{enabledTabs.map((tab) => {
|
|
const tabConfig = TAB_CONFIGS[tab.id];
|
|
if (!tabConfig) return null;
|
|
|
|
return (
|
|
<Tabs.Screen
|
|
key={tab.id}
|
|
name={tab.id}
|
|
options={{ title: t(tabConfig.titleKey) }}
|
|
/>
|
|
);
|
|
})}
|
|
</Tabs>
|
|
);
|
|
}
|