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

@@ -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>
)
}