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.
208 lines
5.8 KiB
208 lines
5.8 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';
|
|
|
|
/** 企业端默认落地「岗位」Tab(与求职端首屏一致) */
|
|
export const unstable_settings = {
|
|
initialRouteName: 'job-management',
|
|
};
|
|
|
|
/** 企业端用 success(绿) 区分求职端 primary(蓝);未选中 -outline,选中填充实心 */
|
|
const ICON_MAP = {
|
|
'enterprise-overview': { outline: 'person-outline', filled: 'person' },
|
|
'job-management': { outline: 'folder-outline', filled: 'folder' },
|
|
'talent-recommendation': { outline: 'people-outline', filled: 'people' },
|
|
'enterprise-chat': { outline: 'chatbubble-outline', filled: 'chatbubble' },
|
|
} as const;
|
|
|
|
type TabRoute = keyof typeof ICON_MAP;
|
|
|
|
function TabItem({
|
|
focused,
|
|
route,
|
|
label,
|
|
onPress,
|
|
onLongPress,
|
|
}: {
|
|
focused: boolean;
|
|
route: TabRoute;
|
|
label: string;
|
|
onPress: () => void;
|
|
onLongPress: () => void;
|
|
}) {
|
|
const { outline, filled } = ICON_MAP[route];
|
|
const translateY = useRef(new Animated.Value(focused ? -2 : 0)).current;
|
|
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();
|
|
return () => {
|
|
translateAnim.stop();
|
|
progressAnim.stop();
|
|
};
|
|
}, [focused, translateY, progress]);
|
|
|
|
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.success} />
|
|
</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>
|
|
);
|
|
}
|
|
|
|
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 导航:岗位 / 人才 / 沟通 / 我的
|
|
* 绿色 success 强调,与求职端蓝色 primary 形成视觉区分。
|
|
*/
|
|
export default function EnterpriseTabLayout() {
|
|
const { setRole } = useRole();
|
|
|
|
useEffect(() => {
|
|
setRole('enterprise');
|
|
}, [setRole]);
|
|
|
|
return (
|
|
<Tabs
|
|
screenOptions={{ headerShown: false }}
|
|
tabBar={(props) => <TabBar {...props} />}
|
|
>
|
|
<Tabs.Screen name="job-management" options={{ title: '岗位' }} />
|
|
<Tabs.Screen name="talent-recommendation" options={{ title: '人才' }} />
|
|
<Tabs.Screen name="enterprise-chat" options={{ title: '沟通' }} />
|
|
<Tabs.Screen name="enterprise-overview" 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.success,
|
|
},
|
|
});
|
|
|