u
This commit is contained in:
1 parent
737aa98d18
commit
3bbccd616d
95 files changed
+858
-22766
No files matched your search
@@ -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
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')}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user