- 创建目标管理演示页面,展示高保真的目标管理界面 - 实现待办事项卡片的横向滑动展示,支持时间筛选功能 - 新增时间轴组件,展示选中日期的具体任务 - 更新底部导航,添加目标管理和演示页面的路由 - 优化个人页面菜单项,提供目标管理的快速访问 - 编写目标管理功能实现文档,详细描述功能和组件架构
279 lines
9.3 KiB
TypeScript
279 lines
9.3 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 { ROUTES } from '@/constants/Routes';
|
|
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
|
|
export default function TabLayout() {
|
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
|
const colorTokens = Colors[theme];
|
|
const pathname = usePathname();
|
|
|
|
return (
|
|
<Tabs
|
|
initialRouteName="coach"
|
|
screenOptions={({ route }) => {
|
|
const routeName = route.name;
|
|
const isSelected = (routeName === 'explore' && pathname === ROUTES.TAB_EXPLORE) ||
|
|
(routeName === 'coach' && pathname === ROUTES.TAB_COACH) ||
|
|
(routeName === 'goals' && pathname === ROUTES.TAB_GOALS) ||
|
|
(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 'explore':
|
|
return { icon: 'magnifyingglass.circle.fill', title: '发现' } as const;
|
|
case 'coach':
|
|
return { icon: 'person.3.fill', title: 'Seal' } as const;
|
|
case 'goals':
|
|
return { icon: 'flag.fill', title: '目标' } 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: 'Seal',
|
|
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,
|
|
}}>
|
|
Seal
|
|
</Text>
|
|
)}
|
|
</View>
|
|
);
|
|
},
|
|
}}
|
|
/>
|
|
<Tabs.Screen
|
|
name="explore"
|
|
options={{
|
|
title: '发现',
|
|
href: null,
|
|
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="goals"
|
|
options={{
|
|
title: '目标',
|
|
tabBarIcon: ({ color }) => {
|
|
const isGoalsSelected = pathname === '/goals';
|
|
return (
|
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
<IconSymbol size={22} name="flag.fill" color={color} />
|
|
{isGoalsSelected && (
|
|
<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>
|
|
);
|
|
}
|