This commit is contained in:
oneao committed 2025-11-07 17:29:49 +08:00
1 parent d6dd5b032d
commit 8a6d29f5d0
16 files changed
+514 -69

No files matched your search

@@ -1,6 +1,7 @@
import type { ComponentType } from 'react'
import type { StyleProp, ViewStyle } from 'react-native'
import { useState } from 'react'
import { Pressable } from 'react-native'
import { EyeIcon, EyeOffIcon } from '@/components/ui/icon'
import { InputField, InputIcon, InputSlot, Input as NativeInput } from '@/components/ui/input'
@@ -52,23 +53,23 @@ export default function Input({
className={className}
style={style}
>
{leftIcon
? (
<InputSlot className="pl-3">
<InputIcon as={leftIcon} />
</InputSlot>
)
: null}
{leftIcon && (
<InputSlot className="pl-3">
<InputIcon as={leftIcon} />
</InputSlot>
)}
<InputField
value={value}
onChangeText={onChange}
placeholder={placeholder}
type={isPassword ? 'password' : 'text'}
type={isPassword ? (showPassword ? 'text' : 'password') : 'text'}
{...rest}
/>
{isPassword && (
<InputSlot className="pr-3" onPress={() => setShowPassword(prev => !prev)}>
<InputIcon as={showPassword ? EyeIcon : EyeOffIcon} />
<InputSlot className="pr-3">
<Pressable onPress={() => setShowPassword(prev => !prev)}>
<InputIcon as={showPassword ? EyeIcon : EyeOffIcon} />
</Pressable>
</InputSlot>
)}
</NativeInput>
@@ -1,5 +1,6 @@
import { Ionicons } from '@expo/vector-icons'
import { usePathname, useRouter } from 'expo-router'
import { useRef } from 'react'
import { Image, Pressable, Text, View } from 'react-native'
import Animated, { SlideInLeft, SlideInRight } from 'react-native-reanimated'
import { SafeAreaView } from 'react-native-safe-area-context'
@@ -31,7 +32,15 @@ export default function StepLayout({ steps, children, showHearder = true }: Step
// 是否显示header
const headerVisible = showHearder !== false && currentStep.showHearder !== false
const enteringAnimation = canGoBack ? SlideInRight.duration(300) : SlideInLeft.duration(300)
const hasMounted = useRef(false)
const enteringAnimation = hasMounted.current
? canGoBack
? SlideInRight.duration(300)
: SlideInLeft.duration(300)
: undefined
hasMounted.current = true
return (
<SafeAreaView style={{ flex: 1 }}>
@@ -0,0 +1,199 @@
import { usePathname, useRouter } 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 { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
import Animated, { useAnimatedStyle, useSharedValue, withSequence, withTiming } from 'react-native-reanimated'
import { scheduleOnRN } from 'react-native-worklets'
interface AppTabbarProps {
data: TabbarItem[]
}
export interface TabbarItem {
icon: keyof typeof icons
iconSize?: number
activeIcon?: keyof typeof icons
name: string
href?: string
onPress?: () => 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 scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}))
useEffect(() => {
if (isFocused) {
scale.value = withSequence(
withTiming(0.9, { duration: 120 }),
withTiming(1, { duration: 160 }),
)
}
}, [isFocused])
const IconComponent = icons[isFocused && activeIcon ? activeIcon : icon]
return (
<Pressable
onPress={onPress}
style={{ flex: 1, alignItems: 'center', justifyContent: 'center', height: '100%' }}
>
<Animated.View style={animatedStyle}>
<IconComponent width={iconSize} height={iconSize} stroke={isFocused ? '#333' : '#aaa'} />
</Animated.View>
</Pressable>
)
}
export default function CustomTabbar({ data }: AppTabbarProps) {
const router = useRouter()
const pathname = usePathname()
const [activeTab, setActiveTab] = useState<string>(() => {
const match = data.find(item => item.href === pathname)
return match?.name ?? data[0]?.name
})
useEffect(() => {
console.log(`pathname:${pathname}`)
}, [pathname])
// 反转方向
const [swipeDirection, setSwipeDirection] = useState<'left' | 'right'>('left')
function updateSwipeDirection(tabName: string) {
const currentIndex = data.findIndex(d => d.name === activeTab)
const targetIndex = data.findIndex(d => d.name === tabName)
if (currentIndex === -1 || targetIndex === -1 || currentIndex === targetIndex)
return
setSwipeDirection(targetIndex > currentIndex ? 'left' : 'right')
setActiveTab(tabName)
}
// 点击调用
const handlPressTab = (item: TabbarItem) => {
updateSwipeDirection(item.name)
setActiveTab(item.name)
if (typeof item.onPress === 'function')
item.onPress()
}
// 定义 JS 线程安全函数
const handlePanTab = (tabName: string) => {
const tab = data.find(item => item.name === tabName)
if (!tab)
return
updateSwipeDirection(tabName)
setActiveTab(tab.name)
if (typeof tab.onPress === 'function') {
tab.onPress()
}
if (tab.href && tab.href !== pathname) {
router.push(tab.href)
}
}
// 手势滑动
const panGesture = Gesture.Pan()
.activeOffsetX([-20, 20])
.failOffsetY([-10, 10])
.onEnd((event) => {
const threshold = 60
const currentIndex = data.findIndex(item => item.name === activeTab)
let targetIndex: number | undefined
if (event.translationX < -threshold && currentIndex < data.length - 1) {
targetIndex = currentIndex + 1
}
else if (event.translationX > threshold && currentIndex > 0) {
targetIndex = currentIndex - 1
}
if (targetIndex !== undefined) {
const tabName = data[targetIndex].name
scheduleOnRN(handlePanTab, tabName)
}
})
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Tabs>
<GestureDetector gesture={panGesture}>
<View style={{ flex: 1 }}>
{(() => {
const currentTab = data.find(d => d.name === activeTab)
if (!currentTab)
return null
return (
<Animated.View
key={activeTab}
style={{
flex: 1,
backgroundColor: 'transparent',
}}
>
<TabSlot />
</Animated.View>
)
})()}
</View>
</GestureDetector>
<TabList asChild>
<View style={{ flexDirection: 'row', backgroundColor: 'white', height: 48, alignItems: 'center' }}>
{data.map((item) => {
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={activeTab === item.name}
onPress={() => handlPressTab(item)}
/>
</TabTrigger>
)
}
return (
<TabButton
key={item.name}
icon={item.icon}
activeIcon={item.activeIcon}
iconSize={item.iconSize}
isFocused={activeTab === item.name}
onPress={() => handlPressTab(item)}
/>
)
})}
</View>
</TabList>
</Tabs>
</GestureHandlerRootView>
)
}
@@ -1,10 +1,10 @@
import { usePathname, useRouter } from 'expo-router'
import { Redirect, usePathname, useRouter, useSegments } 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 { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
import Animated, { SlideInLeft, SlideInRight, useAnimatedStyle, useSharedValue, withSequence, withTiming } from 'react-native-reanimated'
import Animated, { useAnimatedStyle, useSharedValue, withSequence, withTiming } from 'react-native-reanimated'
import { scheduleOnRN } from 'react-native-worklets'
interface AppTabbarProps {
@@ -141,29 +141,25 @@ export default function CustomTabbar({ data }: AppTabbarProps) {
<View style={{ flex: 1 }}>
{(() => {
const currentTab = data.find(d => d.name === activeTab)
if (!currentTab)
return null
return currentTab.href
? (
<Animated.View
key={activeTab}
entering={swipeDirection === 'left' ? SlideInRight.duration(300) : SlideInLeft.duration(300)}
style={{ flex: 1 }}
>
<TabSlot />
</Animated.View>
)
: (
<TabSlot />
)
return (
<Animated.View
style={{
flex: 1,
backgroundColor: 'transparent',
}}
>
<TabSlot />
</Animated.View>
)
})()}
</View>
</GestureDetector>
<TabList asChild>
<View style={{ flexDirection: 'row', backgroundColor: 'white', height: 48, alignItems: 'center' }}>
<View style={{ flexDirection: 'row', height: 48, alignItems: 'center' }}>
{data.map((item) => {
if (item.href) {
return (