This commit is contained in:
oneao committed 2025-11-14 21:58:28 +08:00
1 parent 737aa98d18
commit 3bbccd616d
95 files changed
+858 -22766

No files matched your search

+6 -5
View File
@@ -21,7 +21,7 @@
"axios": "^1.13.2",
"big.js": "^7.0.1",
"dayjs": "^1.11.19",
"expo": "~54.0.20",
"expo": "~54.0.23",
"expo-blur": "~15.0.7",
"expo-constants": "~18.0.10",
"expo-font": "~14.0.9",
@@ -33,10 +33,9 @@
"lucide-react-native": "^0.510.0",
"nativewind": "^4.2.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-calendars": "^1.1313.0",
"react-native-date-picker": "^5.0.13",
"react-native-daterange-picker": "^1.5.1",
"react-native-css-interop": "^0.2.1",
"react-native-dates-picker": "^0.2.0",
"react-native-gesture-handler": "~2.28.0",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
@@ -44,8 +43,10 @@
"react-native-reanimated": "~4.1.3",
"react-native-safe-area-context": "~5.6.1",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.0",
"react-native-svg": "15.12.1",
"react-native-ui-datepicker": "^3.1.2",
"react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1",
"zustand": "^5.0.8"
},
"devDependencies": {
+777 -884
View File
File diff suppressed because it is too large. Load diff
@@ -1 +0,0 @@
nodeLinker: hoisted
@@ -1,5 +1,4 @@
import type { TabbarItem } from '@/components/custom/Tabbar'
import { KeyboardProvider } from 'react-native-keyboard-controller'
import CustomTabbar from '@/components/custom/Tabbar'
import { toast } from '@/utils/toast'
@@ -43,8 +42,6 @@ export default function TabsLayout() {
]
return (
<KeyboardProvider>
<CustomTabbar data={tabbarOptions} />
</KeyboardProvider>
<CustomTabbar data={tabbarOptions} />
)
}
@@ -1,310 +1,19 @@
import type { DateType, ModeType } from 'react-native-dates-picker'
import type { ITheme } from './components/ThemeSelector'
import dayjs from 'dayjs'
import { useCallback, useState } from 'react'
import {
Pressable,
SafeAreaView,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View,
} from 'react-native'
import { useState } from 'react'
import { StyleSheet, View } from 'react-native'
import DateTimePicker from 'react-native-dates-picker'
import LocaleSelector from './components/LocaleSelector'
import ThemeSelector from './components/ThemeSelector'
import 'dayjs/locale/zh-cn'
import 'dayjs/locale/en'
import 'dayjs/locale/de'
import 'dayjs/locale/es'
const Themes: ITheme[] = [
{ mainColor: '#0047FF', activeTextColor: '#fff' },
{ mainColor: '#00D27A', activeTextColor: '#fff' },
{ mainColor: '#F5803E', activeTextColor: '#fff' },
{ mainColor: '#E63757', activeTextColor: '#fff' },
{ mainColor: '#D8E3FF', activeTextColor: '#0047FF' },
{ mainColor: '#CCF6E4', activeTextColor: '#00864E' },
{ mainColor: '#FDE6D8', activeTextColor: '#9D5228' },
{ mainColor: '#FAD7DD', activeTextColor: '#932338' },
]
export default function App() {
const [mode, setMode] = useState<ModeType>('single')
const [timePicker, setTimePicker] = useState(false)
const [date, setDate] = useState<DateType | undefined>()
const [range, setRange] = useState<{
startDate: DateType
endDate: DateType
}>({ startDate: undefined, endDate: undefined })
const [dates, setDates] = useState<DateType[] | undefined>()
const [theme, setTheme] = useState<ITheme | undefined>(Themes[0])
const [locale, setLocale] = useState('zh-cn')
const onChangeMode = useCallback(
(value: ModeType) => {
setDate(undefined)
setRange({ startDate: undefined, endDate: undefined })
setDates(undefined)
setMode(value)
},
[setMode, setDate, setRange, setDates],
)
const onChange = useCallback(
(params) => {
if (mode === 'single' || mode === 'wheel')
setDate(params.date)
else if (mode === 'range')
setRange(params)
else if (mode === 'multiple')
setDates(params.dates)
},
[mode],
)
const [date, setDate] = useState(dayjs())
return (
<SafeAreaView style={styles.container}>
<View style={styles.body}>
<View style={styles.titleContainer}>
<Text style={styles.title}>React Native Dates Picker</Text>
</View>
<ThemeSelector themes={Themes} setTheme={setTheme} />
<LocaleSelector
locale={locale}
setLocale={setLocale}
mainColor={theme?.mainColor}
activeTextColor={theme?.activeTextColor}
/>
<View style={styles.modesContainer}>
<Text style={{ marginRight: 8 }}>Mode:</Text>
<TouchableOpacity
style={[
styles.modeSelect,
{
backgroundColor:
mode === 'single' ? theme?.mainColor : undefined,
},
]}
onPress={() => onChangeMode('single')}
>
<Text
style={[
styles.modeSelectText,
mode === 'single' && { color: theme?.activeTextColor },
]}
>
Single
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.modeSelect,
{
backgroundColor:
mode === 'range' ? theme?.mainColor : undefined,
},
]}
onPress={() => onChangeMode('range')}
>
<Text
style={[
styles.modeSelectText,
mode === 'range' && { color: theme?.activeTextColor },
]}
>
Range
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.modeSelect,
{
backgroundColor:
mode === 'multiple' ? theme?.mainColor : undefined,
},
]}
onPress={() => onChangeMode('multiple')}
>
<Text
style={[
styles.modeSelectText,
mode === 'multiple' && { color: theme?.activeTextColor },
]}
>
Multiple
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.modeSelect,
{
backgroundColor:
mode === 'wheel' ? theme?.mainColor : undefined,
},
]}
onPress={() => onChangeMode('wheel')}
>
<Text
style={[
styles.modeSelectText,
mode === 'wheel' && { color: theme?.activeTextColor },
]}
>
Wheel
</Text>
</TouchableOpacity>
</View>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 10,
marginBottom: 20,
}}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 10,
}}
>
<Switch
value={timePicker}
style={{ transform: [{ scale: 0.65 }] }}
onValueChange={() => setTimePicker(!timePicker)}
disabled={mode !== 'single'}
/>
<Text
style={{
fontSize: 14,
color: '#000',
marginLeft: -8,
textDecorationLine: 'none',
}}
>
Time Picker
</Text>
</View>
<Text style={{ fontSize: 13, color: 'gray' }}>
(Works in Single mode)
</Text>
</View>
<View style={styles.datePickerContainer}>
<View style={styles.datePicker}>
<DateTimePicker
mode={mode}
date={date}
locale={locale}
startDate={range.startDate}
endDate={range.endDate}
dates={dates}
displayFullDays
timePicker={timePicker}
// minDate={range.startDate}
maxDate={new Date()}
// firstDayOfWeek={1}
// columns={['year', 'month', 'day', 'hour', 'minute', 'second']}
onChange={onChange}
headerButtonColor={theme?.mainColor}
selectedItemColor={theme?.mainColor}
selectedTextStyle={{
fontWeight: 'bold',
color: theme?.activeTextColor,
}}
wheelPickerItemStyle={{ paddingHorizontal: 0 }}
todayContainerStyle={{
borderWidth: 1,
}}
/>
<View style={styles.footer}>
{mode === 'single' || mode === 'wheel'
? (
<View style={styles.footerContainer}>
<Text>
{date
? dayjs(date)
.locale(locale)
.format(
timePicker
? 'MMMM, DD, YYYY - HH:mm'
: 'MMMM, DD, YYYY',
)
: '...'}
</Text>
<Pressable
onPress={() => setDate(dayjs() as DateType)}
accessibilityRole="button"
accessibilityLabel="Today"
>
<View
style={[
styles.todayButton,
{ backgroundColor: theme?.mainColor },
]}
>
<Text
style={[
styles.todayButtonText,
{ color: theme?.activeTextColor },
]}
>
Today
</Text>
</View>
</Pressable>
</View>
)
: mode === 'range'
? (
<View style={{ gap: 3 }}>
<Text>
<Text style={{ marginRight: 5, fontWeight: 'bold' }}>
Start Date:
</Text>
{range.startDate
? dayjs(range.startDate)
.locale(locale)
.format('MMMM, DD, YYYY')
: '...'}
</Text>
<Text>
<Text style={{ marginRight: 5, fontWeight: 'bold' }}>
End Date:
</Text>
{range.endDate
? dayjs(range.endDate)
.locale(locale)
.format('MMMM, DD, YYYY')
: '...'}
</Text>
</View>
)
: mode === 'multiple'
? (
<View style={{ gap: 3 }}>
<Text style={{ fontWeight: 'bold' }}>Selected Dates:</Text>
{dates
&& dates.map((d, index) => (
<Text key={index}>
{dayjs(d).locale(locale).format('MMMM, DD, YYYY')}
</Text>
))}
</View>
)
: null}
</View>
</View>
</View>
</View>
</SafeAreaView>
<View style={styles.container}>
<DateTimePicker
mode="single"
date={date}
onChange={params => setDate(params.date)}
/>
</View>
)
}
@@ -313,63 +22,4 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: '#F5FCFF',
},
body: {
flex: 1,
backgroundColor: '#F5FCFF',
alignItems: 'center',
},
titleContainer: {
paddingVertical: 20,
alignItems: 'center',
backgroundColor: '#fff',
marginBottom: 20,
width: '100%',
},
title: { fontSize: 18, fontWeight: 'bold' },
modesContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
marginBottom: 20,
},
modeSelect: {
paddingHorizontal: 15,
paddingVertical: 8,
borderRadius: 20,
},
modeSelectText: {
fontSize: 13,
fontWeight: 'bold',
},
datePickerContainer: {
alignItems: 'center',
},
datePicker: {
width: 375,
backgroundColor: '#fff',
padding: 1,
borderRadius: 15,
shadowRadius: 20,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowOffset: { width: 0, height: 0 },
},
footer: {
paddingHorizontal: 5,
paddingVertical: 5,
marginTop: 15,
},
footerContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
todayButton: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 8,
},
todayButtonText: {
fontWeight: 'bold',
},
})
@@ -1,50 +0,0 @@
import type { KeyboardEvent } from 'react-native'
import React, { useEffect, useRef } from 'react'
import { Animated, Keyboard, Platform } from 'react-native'
interface BottomSectionProps {
children: React.ReactNode
safeMargin?: number // 输入框距离键盘的安全距离
}
const BottomSection: React.FC<BottomSectionProps> = ({ children, safeMargin = 8 }) => {
const translateY = useRef(new Animated.Value(0)).current
useEffect(() => {
const onKeyboardShow = (e: KeyboardEvent) => {
const keyboardHeight = e.endCoordinates.height
console.log(keyboardHeight)
Animated.timing(translateY, {
toValue: -(keyboardHeight - safeMargin),
duration: e.duration ?? 250,
useNativeDriver: true,
}).start()
}
const onKeyboardHide = (e: KeyboardEvent) => {
Animated.timing(translateY, {
toValue: 0,
duration: e.duration ?? 250,
useNativeDriver: true,
}).start()
}
const showSub = Keyboard.addListener(
Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
onKeyboardShow,
)
const hideSub = Keyboard.addListener(
Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
onKeyboardHide,
)
return () => {
showSub.remove()
hideSub.remove()
}
}, [safeMargin])
return <Animated.View style={{ transform: [{ translateY }] }}>{children}</Animated.View>
}
export default BottomSection
@@ -1,68 +0,0 @@
import { Pressable, StyleSheet, Text, View } from 'react-native'
const Locales = ['zh-cn', 'en', 'de', 'es']
interface Props {
locale: string
setLocale: (locale: string) => void
mainColor?: string
activeTextColor?: string
}
export default function LocaleSelector({
locale,
setLocale,
mainColor,
activeTextColor,
}: Props) {
return (
<View style={styles.localeContainer}>
<Text style={{ marginRight: 8 }}>Locale:</Text>
{Locales.map((item, index) => (
<Pressable
key={index}
style={[
styles.localeButton,
item === locale && {
backgroundColor: mainColor,
},
]}
onPress={() => setLocale(item)}
accessibilityRole="button"
accessibilityLabel={item.toUpperCase()}
>
<Text
style={[
styles.localeButtonText,
item === locale && {
fontWeight: 'bold',
color: activeTextColor,
},
]}
>
{item.split('-')[0]?.toUpperCase()}
</Text>
</Pressable>
))}
</View>
)
}
const styles = StyleSheet.create({
localeContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 20,
},
localeButton: {
alignItems: 'center',
justifyContent: 'center',
width: 36,
height: 36,
borderRadius: 36,
margin: 2,
},
localeButtonText: {
fontSize: 15,
},
})
@@ -1,54 +0,0 @@
import { Pressable, StyleSheet, View } from 'react-native'
export interface ITheme {
mainColor: string
activeTextColor: string
}
interface Props {
themes: ITheme[]
setTheme: (theme: ITheme) => void
}
export default function ThemeSelector({ themes = [], setTheme }: Props) {
return (
<View style={styles.themeContainer}>
{themes.map((item, index) => (
<Pressable
key={index}
style={[
styles.themeButton,
{
borderColor: item.activeTextColor,
backgroundColor: item.mainColor,
},
]}
onPress={() => setTheme(item)}
accessibilityRole="button"
accessibilityLabel="Set Active Theme"
/>
))}
</View>
)
}
const styles = StyleSheet.create({
themeContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
marginBottom: 10,
width: 330,
},
themeButton: {
borderWidth: 4,
width: 32,
height: 32,
borderRadius: 32,
margin: 5,
shadowRadius: 20,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowOffset: { width: 0, height: 0 },
},
})
@@ -0,0 +1,11 @@
import { Text, View } from 'react-native'
// 这个页面的作用是作为占位页,避免进入 (tabs) layout 时出现 404 页面。
// 如果想指定默认页面,可以通过 router.replace 或 TabTrigger 指向具体子页面。
export default function Index() {
return (
<View>
<Text>Index</Text>
</View>
)
}
@@ -3,7 +3,6 @@ import type { MoneyCategory } from '@/types/money'
import Big from 'big.js'
import { useEffect, useRef, useState } from 'react'
import { Keyboard, Pressable, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'
import { KeyboardAvoidingView } from 'react-native-keyboard-controller'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { AInput, LucideIcon } from '@/components/common/ao'
import SegmentedTabs from '@/components/common/ao/SegmentedTabs'
@@ -77,44 +76,6 @@ export default function AddMoneyDrawer({
setSelectedCategoryId(item.id)
setShowKeydown(true)
}
const categoryNames = [
'餐饮',
'交通',
'娱乐',
'购物',
'工资',
'投资',
'住房',
'医疗',
'学习',
'旅行',
'通讯',
'宠物',
'礼物',
'数码',
'日用品',
'保险',
'孩子',
'捐赠',
'水电',
'理发',
]
function generateCategories(count: number, type: number): MoneyCategory[] {
const result: MoneyCategory[] = []
for (let i = 1; i <= count; i++) {
const name = categoryNames[Math.floor(Math.random() * categoryNames.length)]
result.push({
id: i,
user_id: 1,
name,
icon: `icon_${name}`,
sort_number: i,
type,
})
}
return result
}
useEffect(() => {
if (isOpen) {
@@ -122,11 +83,7 @@ export default function AddMoneyDrawer({
try {
const res = await getMoneyCategoryApi()
if (res.isSuccess) {
// setCategories(res.data)
setCategories({
income: generateCategories(100, 1),
expense: generateCategories(100, 2),
})
setCategories(res.data)
}
}
catch (err) {
@@ -289,16 +246,6 @@ export default function AddMoneyDrawer({
const [isFocused, setIsFocused] = useState(false)
// 监听键盘收起
useEffect(() => {
const sub = Keyboard.addListener('keyboardDidHide', () => {
if (inputRef.current) {
inputRef.current.blur()
setIsFocused(false)
}
})
return () => sub.remove()
}, [])
return (
<Drawer
@@ -309,7 +256,13 @@ export default function AddMoneyDrawer({
>
<DrawerBackdrop />
<DrawerContent style={{ padding: 0, flex: 1 }}>
<DrawerContent
style={{
padding: 0,
flex: 1,
}}
>
<DrawerHeader
className="flex items-center justify-between"
style={{ paddingTop: insets.top + 8, paddingLeft: 16, paddingRight: 16 }}
@@ -326,7 +279,9 @@ export default function AddMoneyDrawer({
<Text className="text-gray-500 font-medium">取消</Text>
</Pressable>
</DrawerHeader>
<DrawerBody style={{ flex: 1, paddingLeft: 16, paddingRight: 16, marginBottom: 0, backgroundColor: '#fff' }}>
<DrawerBody style={{ flex: 1, paddingLeft: 16, paddingRight: 16, marginBottom: 0, backgroundColor: '#fff',
}}
>
<Grid className="gap-4 mb-8" _extra={{ className: 'grid-cols-4' }}>
{(() => {
const list = selectedIndex === 0 ? categories.income : categories.expense
@@ -367,7 +322,11 @@ export default function AddMoneyDrawer({
</DrawerBody>
{showKeydown && (
<View>
<View style={{
backgroundColor: '#fff',
elevation: 20,
}}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View className="h-28 flex pl-4 pr-4 justify-center">
<View className="flex flex-row items-center justify-between">
@@ -393,7 +352,6 @@ export default function AddMoneyDrawer({
/>
</View>
</TouchableWithoutFeedback>
<View className="flex flex-row flex-wrap" style={{ height: 220 }}>
{keys.map((item, idx) => {
const isFirstCol = idx % 4 === 0
@@ -437,6 +395,9 @@ export default function AddMoneyDrawer({
)
})}
</View>
{isFocused && keyboardHeight > 220 && (
<View style={{ height: keyboardHeight - 220 }} />
)}
</View>
)}
@@ -2,7 +2,6 @@ import type { LayoutChangeEvent } from 'react-native'
import { LinearGradient } from 'expo-linear-gradient'
import React, { useRef, useState } from 'react'
import { ScrollView, Text, View } from 'react-native'
import { KeyboardAvoidingView, KeyboardProvider } from 'react-native-keyboard-controller'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { LucideIcon } from '@/components/common/ao'
import { IconJiaotong } from '@/components/iconfont'
@@ -142,11 +141,13 @@ export default function Money() {
</ScrollView>
</View>
<Fab onPress={() => {
setShowDrawer(true)
}}
<Fab
size="lg"
onPress={() => {
setShowDrawer(true)
}}
>
<FabIcon as={AddIcon} />
<FabIcon as={AddIcon} size="xl" />
</Fab>
<AddMoneyDrawer
@@ -1,58 +0,0 @@
import { LinearGradient } from 'expo-linear-gradient'
import React, { useState } from 'react'
import { ScrollView, Text, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { LucideIcon } from '@/components/common/ao'
import BalanceCard from './components/BalanceCard'
import TabSelector from './components/TabSelector'
import TransactionsList from './components/TransactionsList'
export default function Money() {
const insets = useSafeAreaInsets()
const [selected, setSelected] = useState(0)
return (
<LinearGradient
colors={['#e6f0ff', '#ffffff']}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
className="flex-1"
style={{
paddingTop: insets.top,
paddingBottom: insets.bottom,
paddingLeft: 8,
paddingRight: 8,
}}
>
<View style={{ zIndex: 10 }}>
<View className="flex flex-row justify-between items-center py-2">
<View className="flex flex-row items-center gap-2">
<Text className="font-bold text-lg">2025/11/10</Text>
<LucideIcon icon="CircleChevronDown" size={16} />
</View>
<View className="flex flex-row items-center gap-2">
<LucideIcon icon="SlidersHorizontal" />
<Text>筛选</Text>
</View>
</View>
<TabSelector
options={['明细', '视图']}
selected={selected}
onSelect={tab => setSelected(tab)}
/>
</View>
{/* 可滚动内容 */}
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 20 }}
style={{ marginTop: 0 }}
>
<BalanceCard />
<TransactionsList />
</ScrollView>
</LinearGradient>
)
}
@@ -1,6 +1,6 @@
import { router, Stack } from 'expo-router'
import { ArrowLeft } from 'lucide-react-native'
import EmptyState from '@/components/common/EmptyStatus'
import EmptyState from '@/components/common/EmptyStatus'
export default function NotFound() {
return (
@@ -12,7 +12,7 @@ export default function NotFound() {
description="回首页看看吧!"
buttonText="返回应用首页"
buttonIcon={ArrowLeft}
onButtonPress={() => router.replace('/')}
onButtonPress={() => router.replace('/money')}
/>
</>
)
+14 -5
View File
@@ -17,6 +17,10 @@ import 'react-native-gesture-handler'
SplashScreen.preventAutoHideAsync()
export const unstable_settings = {
initialRouteName: 'money', // 指向你想默认显示的页面
}
export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
return (
<GluestackUIProvider>
@@ -44,14 +48,19 @@ export default function RootLayout() {
useEffect(() => {
if (fontsLoaded) {
SplashScreen.hideAsync()
// 如果没有 token,则跳转登录页
if (!userStore.isLoggedIn()) {
router.replace('/auth')
}
}
}, [fontsLoaded])
if (!fontsLoaded) {
// 字体未加载,显示空白或 Splash
return null
}
if (!userStore.isLoggedIn()) {
// 未登录直接跳到 auth
router.replace('/auth')
return null
}
return (
<KeyboardProvider>
<GluestackUIProvider>
@@ -1,199 +0,0 @@
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,4 +1,5 @@
import { Redirect, usePathname, useRouter, useSegments } from 'expo-router'
import type { Href } from 'expo-router'
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'
@@ -16,7 +17,7 @@ export interface TabbarItem {
iconSize?: number
activeIcon?: keyof typeof icons
name: string
href?: string
href?: Href
onPress?: () => void
}
@@ -30,8 +30,8 @@ function isWhiteListed(url: string) {
}
const alovaInstance = createAlova({
// baseURL: 'http://192.168.5.6:8085/app',
baseURL: 'http://192.168.1.136:8085/app',
baseURL: 'http://192.168.5.6:8085/app',
// baseURL: 'http://192.168.1.136:8085/app',
requestAdapter: axiosRequestAdapter(),
timeout: 5000,
cacheFor: null,