轻松聘
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

231 lines
6.6 KiB

import { Ionicons } from '@expo/vector-icons';
import { Tabs } from 'expo-router/js-tabs';
import type { BottomTabBarProps } from 'expo-router/js-tabs';
import { useEffect, useRef } from 'react';
import { Animated, Easing, Pressable, StyleSheet, View } from 'react-native';
import { colors } from '../../src/theme';
import { useRole } from '../../src/context/RoleContext';
/** 无 index 路由,默认进入「岗位」Tab 作为落地页 */
export const unstable_settings = {
initialRouteName: 'jobs',
};
/** 未选中用 -outline 线性,选中用填充实心 */
const ICON_MAP = {
jobs: { outline: 'briefcase-outline', filled: 'briefcase' },
chat: { outline: 'chatbubble-outline', filled: 'chatbubble' },
growth: { outline: 'trending-up-outline', filled: 'trending-up' },
profile: { outline: 'person-outline', filled: 'person' },
} as const;
type TabRoute = keyof typeof ICON_MAP;
/**
* 单个 Tab 项:极简图标 + 文字。
* 点击选中时:
* - 图标/文字从「灰色描边」平滑渐变到「主色填充」
* (灰层 + 主色层叠放,opacity 交叉淡入,200ms,纯原生驱动)
* - 图标上浮 2px(150ms)
* - 按压反馈:轻微缩放 0.92
*/
function TabItem({
focused,
route,
label,
onPress,
onLongPress,
}: {
focused: boolean;
route: TabRoute;
label: string;
onPress: () => void;
onLongPress: () => void;
}) {
const { outline, filled } = ICON_MAP[route];
// 图标纵向位移:选中上浮 2px,未选中复位
const translateY = useRef(new Animated.Value(focused ? -2 : 0)).current;
// 选中进度:0 = 未选中,1 = 选中
const progress = useRef(new Animated.Value(focused ? 1 : 0)).current;
useEffect(() => {
const translateAnim = Animated.timing(translateY, {
toValue: focused ? -2 : 0,
duration: 150,
easing: Easing.out(Easing.ease),
useNativeDriver: true,
});
const progressAnim = Animated.timing(progress, {
toValue: focused ? 1 : 0,
duration: 200,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
});
translateAnim.start();
progressAnim.start();
// 清理:组件卸载或 focused 变化前停止动画,避免更新已卸载的节点
return () => {
translateAnim.stop();
progressAnim.stop();
};
}, [focused, translateY, progress]);
// 未选中层(灰 + 描边)透明度:1 → 0;选中层(主色 + 填充)透明度:0 → 1
const inactiveOpacity = progress.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
});
return (
<Pressable
style={({ pressed }) => [styles.tabItem, pressed && styles.tabItemPressed]}
onPress={onPress}
onLongPress={onLongPress}
accessibilityRole="button"
accessibilityState={{ selected: focused }}
>
<Animated.View style={{ transform: [{ translateY }] }}>
<View style={styles.iconBox}>
{/* 未选中层:灰色描边图标 */}
<Animated.View
style={[StyleSheet.absoluteFill, styles.center, { opacity: inactiveOpacity }]}
>
<Ionicons name={outline} size={22} color={colors.textHint} />
</Animated.View>
{/* 选中层:主色填充图标 */}
<Animated.View style={[StyleSheet.absoluteFill, styles.center, { opacity: progress }]}>
<Ionicons name={filled} size={22} color={colors.primary} />
</Animated.View>
</View>
</Animated.View>
<View style={styles.labelBox}>
{/* 未选中文字:灰色常规字重 */}
<Animated.Text style={[styles.tabLabel, styles.labelLayer, { opacity: inactiveOpacity }]}>
{label}
</Animated.Text>
{/* 选中文字:主色加粗 */}
<Animated.Text
style={[styles.tabLabel, styles.tabLabelActive, styles.labelLayer, { opacity: progress }]}
>
{label}
</Animated.Text>
</View>
</Pressable>
);
}
/**
* 底部 Tab 栏:占据底部布局空间(非悬浮),纯白底、无分割线,
* 内容只在 tab 栏上方滚动,不会从图标后面穿出。
*/
function TabBar({ state, descriptors, navigation, insets }: BottomTabBarProps) {
return (
<View style={[styles.tabBar, { paddingBottom: insets.bottom + 4 }]}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const focused = state.index === index;
const label =
typeof options.tabBarLabel === 'string' ? options.tabBarLabel : options.title ?? '';
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!focused && !event.defaultPrevented) {
navigation.navigate(route.name);
}
};
const onLongPress = () =>
navigation.emit({ type: 'tabLongPress', target: route.key });
return (
<TabItem
key={route.key}
route={route.name as TabRoute}
focused={focused}
label={label}
onPress={onPress}
onLongPress={onLongPress}
/>
);
})}
</View>
);
}
/**
* 底部 Tab 导航:4 个 Tab(岗位/沟通/成长/我的),高级感极简风格
*/
export default function TabLayout() {
const { setRole } = useRole();
useEffect(() => {
setRole('candidate');
}, [setRole]);
return (
<Tabs
screenOptions={{ headerShown: false }}
tabBar={(props) => <TabBar {...props} />}
>
<Tabs.Screen name="jobs" options={{ title: '岗位' }} />
<Tabs.Screen name="chat" options={{ title: '沟通' }} />
<Tabs.Screen name="growth" options={{ title: '成长' }} />
<Tabs.Screen name="profile" options={{ title: '我的' }} />
</Tabs>
);
}
const styles = StyleSheet.create({
// 纯白底、无分割线、无阴影,占底部布局空间
tabBar: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
paddingTop: 4,
},
tabItem: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 2,
},
tabItemPressed: {
transform: [{ scale: 0.92 }],
},
iconBox: {
width: 22,
height: 22,
},
center: {
alignItems: 'center',
justifyContent: 'center',
},
labelBox: {
height: 14,
marginTop: 1,
alignSelf: 'stretch',
},
labelLayer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
},
tabLabel: {
fontSize: 11,
lineHeight: 14,
fontWeight: '400',
textAlign: 'center',
color: colors.textHint,
},
tabLabelActive: {
fontWeight: '600',
color: colors.primary,
},
});