Files
digital-pilates/app/(tabs)/_layout.tsx
richarjiang 849447c5da feat: 引入路由常量并更新相关页面导航
- 新增 ROUTES 常量文件,集中管理应用路由
- 更新多个页面的导航逻辑,使用 ROUTES 常量替代硬编码路径
- 修改教练页面和今日训练页面的路由,提升代码可维护性
- 优化标签页和登录页面的导航,确保一致性和易用性
2025-08-18 10:05:22 +08:00

246 lines
8.2 KiB
TypeScript

import * as Haptics from 'expo-haptics';
import { Tabs, usePathname } from 'expo-router';
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { IconSymbol } from '@/components/ui/IconSymbol';
import { Colors } from '@/constants/Colors';
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
import { useColorScheme } from '@/hooks/useColorScheme';
import { ROUTES } from '@/constants/Routes';
export default function TabLayout() {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const pathname = usePathname();
return (
<Tabs
screenOptions={({ route }) => {
const routeName = route.name;
const isSelected = (routeName === 'index' && pathname === ROUTES.TAB_HOME) ||
(routeName === 'coach' && pathname === ROUTES.TAB_COACH) ||
(routeName === 'statistics' && pathname === ROUTES.TAB_STATISTICS) ||
pathname.includes(routeName);
return {
headerShown: false,
tabBarActiveTintColor: colorTokens.tabIconSelected,
tabBarButton: (props) => {
const { onPress } = props;
const handlePress = (event: any) => {
if (process.env.EXPO_OS === 'ios') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
onPress && onPress(event);
};
// 基于 routeName 设置图标与标题,避免 tabBarIcon 的包装导致文字裁剪
const getIconAndTitle = () => {
switch (routeName) {
case 'index':
return { icon: 'magnifyingglass.circle.fill', title: '发现' } as const;
case 'coach':
return { icon: 'person.3.fill', title: 'Bot' } as const;
case 'statistics':
return { icon: 'chart.pie.fill', title: '统计' } as const;
case 'personal':
return { icon: 'person.fill', title: '个人' } as const;
default:
return { icon: 'circle', title: '' } as const;
}
};
const { icon, title } = getIconAndTitle();
const activeContentColor = colorTokens.onPrimary;
const inactiveContentColor = colorTokens.tabIconDefault;
return (
<TouchableOpacity
onPress={handlePress}
accessibilityRole="button"
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
marginHorizontal: 6,
marginVertical: 10,
borderRadius: 25,
backgroundColor: isSelected ? colorTokens.tabBarActiveBackground : 'transparent',
paddingHorizontal: isSelected ? 16 : 10,
paddingVertical: 8,
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<IconSymbol
size={22}
name={icon as any}
color={isSelected ? activeContentColor : inactiveContentColor}
/>
{isSelected && !!title && (
<Text
style={{
color: activeContentColor,
fontSize: 12,
fontWeight: '600',
marginLeft: 6,
}}
// 选中态下不限制行数,避免大屏布局下被裁剪成省略号
numberOfLines={0 as any}
>
{title}
</Text>
)}
</View>
</TouchableOpacity>
);
},
tabBarStyle: {
position: 'absolute',
bottom: TAB_BAR_BOTTOM_OFFSET,
height: TAB_BAR_HEIGHT,
borderRadius: 34,
backgroundColor: colorTokens.tabBarBackground,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 10,
elevation: 5,
paddingHorizontal: 10,
paddingTop: 0,
paddingBottom: 0,
marginHorizontal: 20,
width: '90%',
alignSelf: 'center',
},
tabBarItemStyle: {
backgroundColor: 'transparent',
height: TAB_BAR_HEIGHT,
marginTop: 0,
marginBottom: 0,
paddingTop: 0,
paddingBottom: 0,
},
tabBarShowLabel: false,
};
}}>
<Tabs.Screen
name="coach"
options={{
title: 'Bot',
tabBarIcon: ({ color }) => {
const isCoachSelected = pathname === '/coach';
return (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<IconSymbol size={22} name="person.3.fill" color={color} />
{isCoachSelected && (
<Text
numberOfLines={1}
style={{
color: color,
fontSize: 12,
fontWeight: '600',
marginLeft: 6,
textAlign: 'center',
flexShrink: 0,
}}>
Bot
</Text>
)}
</View>
);
},
}}
/>
<Tabs.Screen
name="index"
options={{
title: '发现',
tabBarIcon: ({ color }) => {
const isHomeSelected = pathname === '/' || pathname === '/index';
return (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<IconSymbol size={22} name="magnifyingglass.circle.fill" color={color} />
{isHomeSelected && (
<Text
numberOfLines={1}
style={{
color: color,
fontSize: 12,
fontWeight: '600',
marginLeft: 6,
textAlign: 'center',
flexShrink: 0,
}}>
</Text>
)}
</View>
);
},
}}
/>
<Tabs.Screen
name="statistics"
options={{
title: '统计',
tabBarIcon: ({ color }) => {
const isStatisticsSelected = pathname === '/statistics';
return (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<IconSymbol size={22} name="chart.pie.fill" color={color} />
{isStatisticsSelected && (
<Text
numberOfLines={1}
style={{
color: color,
fontSize: 12,
fontWeight: '600',
marginLeft: 6,
textAlign: 'center',
flexShrink: 0,
}}>
</Text>
)}
</View>
);
},
}}
/>
<Tabs.Screen
name="personal"
options={{
title: '个人',
tabBarIcon: ({ color }) => {
const isPersonalSelected = pathname === '/personal';
return (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<IconSymbol size={22} name="person.fill" color={color} />
{isPersonalSelected && (
<Text
numberOfLines={1}
style={{
color: color,
fontSize: 12,
fontWeight: '600',
marginLeft: 6,
textAlign: 'center',
flexShrink: 0,
}}>
</Text>
)}
</View>
);
},
}}
/>
</Tabs>
);
}