This commit is contained in:
oneao committed 2025-11-02 22:08:05 +08:00
1 parent 1d6bb2b7f5
commit 458f0cd6ac
55 files changed
+1052 -999

No files matched your search

@@ -0,0 +1,137 @@
import { usePathname } from 'expo-router'
import { TabList, Tabs, TabSlot, TabTrigger } from 'expo-router/ui'
import { icons } from 'lucide-react-native'
import { useEffect, useState } from 'react'
import { Pressable, View } from 'react-native'
import Animated, {
useAnimatedStyle,
useSharedValue,
withSequence,
withTiming,
} from 'react-native-reanimated'
export interface TabbarItem {
icon: keyof typeof icons
iconSize?: number
activeIcon?: keyof typeof icons
name: string
href?: string
onPress?: () => void
}
interface AppTabbarProps {
data: TabbarItem[]
onTabPress?: (item: TabbarItem) => void
}
function TabButton({
icon,
activeIcon,
iconSize = 24,
isFocused = false,
onPress,
}: {
icon: keyof typeof icons
activeIcon?: keyof typeof icons
iconSize?: number
isFocused?: boolean
onPress?: () => void
}) {
const activeColor = '#333'
const inactiveColor = '#aaa'
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}))
const IconComponent = icons[isFocused && activeIcon ? activeIcon : icon]
const handlePress = () => {
scale.value = withSequence(
withTiming(0.9, { duration: 200 }),
withTiming(1, { duration: 200 }),
)
onPress?.()
}
return (
<Pressable
onPress={handlePress}
className="flex-1 items-center justify-center h-full"
>
<Animated.View style={animatedStyle}>
<IconComponent
width={iconSize}
height={iconSize}
stroke={isFocused ? activeColor : inactiveColor}
/>
</Animated.View>
</Pressable>
)
}
export default function AppTabbar({ data, onTabPress }: AppTabbarProps) {
const pathname = usePathname()
// 初始选中 Tab
const defaultTab = data.find(item => item.href === pathname)?.name
?? data.find(item => item.href)?.name
?? data[0]?.name
const [activeTab, setActiveTab] = useState<string>(defaultTab)
// 当 pathname 改变(路由跳转)时,同步高亮
useEffect(() => {
const matchTab = data.find(item => item.href === pathname)
if (matchTab) {
setActiveTab(matchTab.name)
}
// 如果没有匹配的路由,保持当前的 activeTab 不变
// 这样无 href 的项就能保持选中状态
}, [pathname, data])
const handleTabPress = (item: TabbarItem) => {
setActiveTab(item.name)
item.onPress?.()
onTabPress?.(item)
}
return (
<Tabs>
<TabSlot />
<TabList asChild>
<View className="flex-row bg-white h-12 items-center">
{data.map((item) => {
const isFocused = activeTab === item.name
if (item.href) {
return (
<TabTrigger key={item.name} name={item.name} href={item.href} asChild>
<TabButton
icon={item.icon}
activeIcon={item.activeIcon}
iconSize={item.iconSize}
isFocused={isFocused}
onPress={() => handleTabPress(item)}
/>
</TabTrigger>
)
}
return (
<TabButton
key={item.name}
icon={item.icon}
activeIcon={item.activeIcon}
iconSize={item.iconSize}
onPress={() => handleTabPress(item)}
isFocused={isFocused}
/>
)
})}
</View>
</TabList>
</Tabs>
)
}