This commit is contained in:
oneao committed 2026-01-26 22:31:12 +08:00
1 parent 5a79414319
commit 8663cb2e3d
28 files changed
+744 -756

No files matched your search

@@ -1,43 +1,40 @@
import type { icons } from 'lucide-react-native'
import type { SortableGridRenderItem } from 'react-native-sortables'
import type { NavItem } from '@/store/navigation'
import { Slot, usePathname, useRouter } from 'expo-router'
import { useCallback, useEffect, useState } from 'react'
import { Pressable, Text, TouchableOpacity, Vibration, View } from 'react-native'
import { Text, TouchableOpacity, View } from 'react-native'
import Animated, { useAnimatedRef } from 'react-native-reanimated'
import Sortable from 'react-native-sortables'
import { ADrawer, LucideIcon } from '@/components/common/ao'
interface NavItem {
label: string
path: string
icon: keyof typeof icons
}
const NAV_MENUS: NavItem[] = [
{ label: '记账', path: '/money', icon: 'CircleDollarSign' },
{ label: '恋爱记', path: '/love', icon: 'Heart' },
{ label: '聊天', path: '/chat', icon: 'MessageCircle' },
{ label: '个人中心', path: '/user', icon: 'CircleUserRound' },
]
import { useNavStore } from '@/store/navigation'
export default function TabsLayout() {
const scrollableRef = useAnimatedRef<Animated.ScrollView>()
const router = useRouter()
const pathname = usePathname()
const navStore = useNavStore()
const [showNavDrawer, setShowNavDrawer] = useState(false)
const [draggingId, setDraggingId] = useState<string>('')
const [currentNav, setCurrentNav] = useState<NavItem>({
label: '',
path: '',
icon: 'AArrowDown',
label: 'Welcome',
path: '/',
icon: 'Sparkles',
})
useEffect(() => {
const current = NAV_MENUS.find(item => item.path === pathname)
const current = navStore.navMenus.find(item => item.path === pathname)
if (current) {
setCurrentNav(current)
}
else {
setCurrentNav({
label: 'Welcome',
path: '/',
icon: 'Sparkles',
})
}
}, [pathname])
const refreshPage = () => {
@@ -46,15 +43,14 @@ export default function TabsLayout() {
const renderNavItem = useCallback<SortableGridRenderItem<NavItem>>(
({ item }) => (
<Pressable
<TouchableOpacity
className="
flex
flex-col
items-center
justify-center
bg-[white]
bg-[#f9f9f9]
rounded-md
shadow-md
p-3
"
onPress={() => {
@@ -66,7 +62,7 @@ export default function TabsLayout() {
<Text className="text-sm mt-2">
{item.label}
</Text>
</Pressable>
</TouchableOpacity>
),
[],
)
@@ -86,6 +82,7 @@ export default function TabsLayout() {
px-2
gap-4
"
style={{ zIndex: 0 }}
>
<View className="bg-white rounded-md shadow-lg h-12 w-12">
<TouchableOpacity className="w-full h-full items-center justify-center">
@@ -128,11 +125,15 @@ export default function TabsLayout() {
columnGap={16}
rowGap={16}
columns={3}
data={NAV_MENUS}
data={navStore.navMenus}
keyExtractor={item => item.path}
renderItem={renderNavItem}
scrollableRef={scrollableRef}
autoScrollActivationOffset={200}
showDropIndicator
onDragEnd={(dragData) => {
navStore.setNavMenus(dragData.data)
}}
/>
</Animated.ScrollView>
</ADrawer>
Whitespace-only changes.
+23 -9
View File
@@ -1,20 +1,34 @@
import { useRouter } from 'expo-router'
import { Text, View } from 'react-native'
import { Image, Text, View } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { AButton } from '@/components/common/ao'
// 这个页面的作用是作为占位页,避免进入 (tabs) layout 时出现 404 页面。
// 如果想指定默认页面,可以通过 router.replace 或 TabTrigger 指向具体子页面。
export default function Index() {
const router = useRouter()
return (
<SafeAreaView>
<View>
<AButton onPress={() => {
router.push('/money')
}}
<SafeAreaView className="flex-1 bg-white">
<View className="flex-1 items-center justify-center px-6">
<Image
source={require('@/static/images/welcome.png')}
className="w-86 h-48 mb-6"
resizeMode="contain"
/>
<Text className="text-2xl font-semibold text-gray-900 mb-3">
🎉 欢迎使用 OneAO
</Text>
<Text className="text-base text-gray-500 text-center mb-10">
记录生活,记录情绪,也记录每一笔重要的支出
</Text>
<AButton
className="w-full"
onPress={() => router.replace('/money')}
>
测试
开始使用
</AButton>
</View>
</SafeAreaView>
@@ -1,20 +1,17 @@
import { useFocusEffect, useRouter } from 'expo-router'
import React, { useCallback, useEffect, useState } from 'react'
import { Image, Text, TouchableOpacity, View } from 'react-native'
import { Drawer } from 'react-native-drawer-layout'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { ADatePicker, AListItem, LucideIcon } from '@/components/common/ao'
import { ADatePicker, ADrawer, AListItem, LucideIcon } from '@/components/common/ao'
import { Fab, FabIcon } from '@/components/ui/fab'
import { AddIcon } from '@/components/ui/icon'
import { queryMoneyRecordsApi } from '@/request/api/money/record'
import { useAppStore } from '@/store/app'
import { useMoneyRecordStore } from '@/store/moneyRecord'
import { getCurrentYearMonth } from '@/utils/time'
import { toast } from '@/utils/toast'
import TransactionsList from './components/TransactionsList'
export default function Money() {
const appStore = useAppStore()
const moneyRecordStore = useMoneyRecordStore()
const router = useRouter()
// 安全高度
@@ -47,12 +44,6 @@ export default function Money() {
})
}
useEffect(() => {
if (!appStore.ready)
return
getMoneyRecords()
}, [appStore.ready, searchParams])
useFocusEffect(
useCallback(() => {
// 页面显示 / 返回时
@@ -68,120 +59,78 @@ export default function Money() {
<View
className="flex-1"
>
<Drawer
open={showActionsMenu}
onOpen={() => setShowActionsMenu(true)}
onClose={() => setShowActionsMenu(false)}
drawerPosition="right"
drawerStyle={{
width: '70%',
}}
renderDrawerContent={() => {
return (
<SafeAreaView className="flex-col px-3">
<Image
source={require('@/static/images/logo.png')}
style={{ width: 40, height: 40 }}
className="self-center"
/>
<View className="mt-2">
<Text className="text-gray-600 font-semibold text-sm">管理功能</Text>
<View className="p-1">
<AListItem
icon="LayoutGrid"
text="类别设置"
onPress={() => {
router.navigate('/money/category/setting')
}}
/>
</View>
</View>
<View className="mt-2">
<Text className="text-gray-600 font-semibold text-sm">统计分析</Text>
<View className="p-1">
<AListItem icon="ChartSpline" text="图表分析" />
</View>
</View>
</SafeAreaView>
)
<View
style={{
paddingTop: insets.top,
backgroundColor: '#fff',
elevation: 3,
}}
>
<View
style={{
paddingTop: insets.top,
backgroundColor: '#fff',
elevation: 3,
}}
>
<View className="flex flex-row justify-between items-center px-3">
<TouchableOpacity
onPress={() => setShowDatePicker(true)}
className="flex flex-row items-center gap-1"
>
<Text className="font-semibold text-lg">{searchParams.record_time}</Text>
<LucideIcon icon="ChevronDown" size={16} color="#666" clickable={false} />
</TouchableOpacity>
<TouchableOpacity
className="flex flex-row items-center gap-2"
onPress={() => {
setShowActionsMenu(true)
}}
>
<LucideIcon icon="Menu" size={24} clickable={false} />
</TouchableOpacity>
</View>
<View
className="px-3 py-3"
<View className="flex flex-row justify-between items-center px-3">
<TouchableOpacity
onPress={() => setShowDatePicker(true)}
className="flex flex-row items-center gap-1"
>
<View
className="flex flex-row gap-4"
style={{
borderBottomLeftRadius: 16,
borderBottomRightRadius: 18,
}}
>
<View className="flex-1">
<Text className="text-gray-500 text-sm">收入</Text>
<Text
className="text-lg font-semibold mt-0.5"
numberOfLines={1}
ellipsizeMode="tail"
>
{moneyRecordStore.getTotalIncome()}
</Text>
</View>
<Text className="font-semibold text-lg">{searchParams.record_time}</Text>
<LucideIcon icon="ChevronDown" size={16} color="#666" clickable={false} />
</TouchableOpacity>
<View className="flex-1">
<Text className="text-gray-500 text-sm">支出</Text>
<Text
className="text-lg font-semibold mt-0.5"
numberOfLines={1}
ellipsizeMode="tail"
>
{moneyRecordStore.getTotalExpense()}
</Text>
</View>
<TouchableOpacity
className="flex flex-row items-center gap-2"
onPress={() => {
setShowActionsMenu(true)
}}
>
<LucideIcon icon="Menu" size={24} clickable={false} />
</TouchableOpacity>
<View className="flex-1">
<Text className="text-gray-500 text-sm">结余</Text>
<Text
className="text-lg font-semibold mt-0.5"
numberOfLines={1}
ellipsizeMode="tail"
>
{moneyRecordStore.getTotalBalance()}
</Text>
</View>
</View>
<View
className="px-3 py-3"
>
<View
className="flex flex-row gap-4"
style={{
borderBottomLeftRadius: 16,
borderBottomRightRadius: 18,
}}
>
<View className="flex-1">
<Text className="text-gray-500 text-sm">收入</Text>
<Text
className="text-lg font-semibold mt-0.5"
numberOfLines={1}
ellipsizeMode="tail"
>
{moneyRecordStore.getTotalIncome()}
</Text>
</View>
{/* <View
<View className="flex-1">
<Text className="text-gray-500 text-sm">支出</Text>
<Text
className="text-lg font-semibold mt-0.5"
numberOfLines={1}
ellipsizeMode="tail"
>
{moneyRecordStore.getTotalExpense()}
</Text>
</View>
<View className="flex-1">
<Text className="text-gray-500 text-sm">结余</Text>
<Text
className="text-lg font-semibold mt-0.5"
numberOfLines={1}
ellipsizeMode="tail"
>
{moneyRecordStore.getTotalBalance()}
</Text>
</View>
</View>
{/* <View
className="flex-row items-center gap-3 mt-2"
style={{
borderBottomLeftRadius: 8,
@@ -196,58 +145,98 @@ export default function Money() {
</View>
<Text className="text-xs text-blue-500">点击设置</Text>
</View> */}
</View>
</View>
</View>
<View className="flex-1 mt-2">
<TransactionsList
records={moneyRecordStore.records}
onUpdate={(data) => {
router.navigate({
pathname: '/money/record/editor',
params: { id: data.id },
})
}}
onDetail={(id) => {
router.navigate({
pathname: '/money/record/detail',
params: { id },
})
}}
onDelete={(id) => {
moneyRecordStore.deleteMoneyRecord(id)
}}
onRefreshData={() => {
getMoneyRecords()
}}
/>
</View>
<Fab
size="lg"
onPress={() => {
router.navigate('/money/record/editor')
}}
>
<FabIcon as={AddIcon} size="xl" />
</Fab>
<ADatePicker
show={showDatePicker}
mode="month"
date={searchParams.record_time}
onConfirm={(val) => {
setSearchParams({
...searchParams,
record_time: val,
<View className="flex-1 mt-2">
<TransactionsList
records={moneyRecordStore.records}
onUpdate={(data) => {
router.navigate({
pathname: '/money/record/editor',
params: { id: data.id },
})
setShowDatePicker(false)
}}
onClose={() => {
setShowDatePicker(false)
onDetail={(id) => {
router.navigate({
pathname: '/money/record/detail',
params: { id },
})
}}
onDelete={(id) => {
moneyRecordStore.deleteMoneyRecord(id)
}}
onRefreshData={() => {
getMoneyRecords()
}}
/>
</Drawer>
</View>
<Fab
size="lg"
onPress={() => {
router.navigate('/money/record/editor')
}}
className="mb-20"
>
<FabIcon as={AddIcon} size="xl" />
</Fab>
<ADatePicker
show={showDatePicker}
mode="month"
date={searchParams.record_time}
onConfirm={(val) => {
setSearchParams({
...searchParams,
record_time: val,
})
setShowDatePicker(false)
}}
onClose={() => {
setShowDatePicker(false)
}}
/>
<ADrawer
visible={showActionsMenu}
hideModal={() => {
setShowActionsMenu(false)
}}
menuPosition="right"
height="60%"
showHeader={false}
>
<SafeAreaView className="flex-col px-3">
<Image
source={require('@/static/images/logo.png')}
style={{ width: 40, height: 40 }}
className="self-center"
/>
<View className="mt-2">
<Text className="text-gray-600 font-semibold text-sm">管理功能</Text>
<View className="p-1">
<AListItem
icon="LayoutGrid"
text="类别设置"
onPress={() => {
router.navigate('/money/category/setting')
}}
/>
</View>
</View>
<View className="mt-2">
<Text className="text-gray-600 font-semibold text-sm">统计分析</Text>
<View className="p-1">
<AListItem icon="ChartSpline" text="图表分析" />
</View>
</View>
</SafeAreaView>
</ADrawer>
</View>
)
}
@@ -1,152 +0,0 @@
import { useRouter } from 'expo-router'
import { useState } from 'react'
import { Image, Text, View } from 'react-native'
import { AButton, ACheckbox, AForm, AFormItem, AInput } from '@/components/common/ao'
import CustomHeader from '@/components/custom/Header'
import { IconQq, IconWeixin, IconZhiwen } from '@/components/iconfont'
import { Divider } from '@/components/ui/divider'
import useFormValidation from '@/hooks/useFormValidation'
import { loginDefaultApi } from '@/request/api/auth'
import { useAppStore } from '@/store/app'
import { useUserStore } from '@/store/user'
import { toast } from '@/utils/toast'
interface LoginForm {
account: string
password: string
rememberMe: boolean
socialType?: 'qq' | 'weixin' | 'fingerprint'
}
export default function AuthLogin() {
const router = useRouter()
const userStore = useUserStore()
const appStore = useAppStore()
const [loading, setLoading] = useState<boolean>(false)
const [form, setForm] = useState<LoginForm>({
account: userStore.userInfo?.account || '',
password: '',
rememberMe: true,
})
const handleInputChange = (field: keyof LoginForm, value: string) => {
setForm(prev => ({ ...prev, [field]: value }))
}
const rules = {
account: { required: true, message: '账号是必填项' },
password: { required: true, message: '密码是必填项' },
}
const { isValid } = useFormValidation(form, rules)
const loginDefault = () => {
setLoading(true)
loginDefaultApi({
account: form.account,
password: form.password,
}).then((res) => {
if (res.isSuccess) {
toast.show({
title: '登录成功',
})
userStore.setUserInfo({
account: form.account,
id: res.data.userId,
seqNo: res.data.seqNo,
username: res.data.username,
gender: res.data.gender,
token: res.data.token,
refreshToken: res.data.refreshToken,
rememberMe: form.rememberMe,
})
appStore.init()
router.replace('/money')
}
else {
toast.show({
title: res.message || '登录失败',
})
}
}).catch((_err) => {
}).finally(() => {
setLoading(false)
})
}
const loginQq = () => {
toast.show({ title: '功能待开发,请稍等. . .' })
}
const loginWx = () => {
toast.show({ title: '功能待开发,请稍等. . .' })
}
const loginZhiwen = () => {
toast.show({ title: '功能待开发,请稍等. . .' })
}
return (
<View className="flex-1">
<CustomHeader title="登录" />
<View className="flex flex-col items-center px-6 h-full">
<Image
source={require('@/static/images/logo.png')}
style={{ width: 120, height: 120 }}
className="my-12"
/>
<AForm form={form}>
<AFormItem label="账号">
<AInput
value={form.account}
onChangeText={text => handleInputChange('account', text)}
/>
</AFormItem>
<AFormItem label="密码">
<AInput
isPassword
value={form.password}
onChangeText={text => handleInputChange('password', text)}
/>
</AFormItem>
</AForm>
<View className="w-full flex-row justify-between items-center mb-6">
<ACheckbox
isChecked={form.rememberMe}
onChange={checked => setForm(prev => ({ ...prev, rememberMe: checked }))}
label="记住我"
/>
<Text className="text-blue-500">忘记密码?</Text>
</View>
<AButton className="w-full" onPress={loginDefault} disabled={!isValid} loading={loading}>
登录
</AButton>
<View className="flex-row items-center my-8 w-full">
<View className="flex-1">
<Divider />
</View>
<Text className="mx-3 text-gray-500 text-sm">社交账号</Text>
<View className="flex-1">
<Divider />
</View>
</View>
<View className="w-full flex-row justify-between items-center">
<IconQq size={32} onPress={loginQq} />
<IconWeixin size={32} onPress={loginWx} />
<IconZhiwen size={32} onPress={loginZhiwen} />
</View>
</View>
</View>
)
}
@@ -1,129 +0,0 @@
import type { AFormRef } from '@/components/common/ao'
import { useRouter } from 'expo-router'
import { Lock, UserRound } from 'lucide-react-native'
import { useRef, useState } from 'react'
import { Image, ScrollView, View } from 'react-native'
import { AButton, AForm, AFormItem, AInput } from '@/components/common/ao'
import CustomHeader from '@/components/custom/Header'
import { registerDefaultApi } from '@/request/api/auth'
import { toast } from '@/utils/toast'
interface RegisterForm {
username: string
password: string
confirmPassword: string
}
export default function AuthRegister() {
const router = useRouter()
const scrollRef = useRef<ScrollView>(null)
const [form, setForm] = useState<RegisterForm>({
username: '',
password: '',
confirmPassword: '',
})
const [loading, setLoading] = useState(false)
const formRef = useRef<AFormRef>(null)
const handleInputChange = (field: keyof RegisterForm, value: string) => {
setForm(prev => ({ ...prev, [field]: value }))
}
const handleRegister = async () => {
if (formRef.current?.validate()) {
setLoading(true)
await registerDefaultApi({
account: form.username,
password: form.password,
}).then((res) => {
if (res.isSuccess) {
toast.show({
title: '注册成功',
})
router.replace('/auth/login')
}
else {
toast.show({
title: res.message || '注册失败',
})
}
}).catch((_err) => {
}).finally(() => {
setLoading(false)
})
}
}
return (
<View className="flex-1">
<CustomHeader title="注册" />
<ScrollView ref={scrollRef} style={{ flex: 1 }}>
<View className="flex flex-col items-center px-6 bg-white ">
<Image
source={require('@/static/images/logo.png')}
style={{ width: 120, height: 120 }}
className="my-12"
/>
<AForm ref={formRef} form={form} scrollRef={scrollRef}>
<AFormItem
name="username"
label="账号"
isRequired
>
<AInput
placeholder="请输入账号"
leftIcon={UserRound}
value={form.username}
onChangeText={text => handleInputChange('username', text)}
/>
</AFormItem>
<AFormItem
name="password"
label="密码"
isRequired
>
<AInput
placeholder="请输入密码"
leftIcon={Lock}
isPassword
value={form.password}
onChangeText={text => handleInputChange('password', text)}
/>
</AFormItem>
<AFormItem
name="confirmPassword"
label="确认密码"
isRequired
rules={[
{
validator: val => val === form.password,
message: '两次密码不一致',
},
]}
>
<AInput
placeholder="请再次输入密码"
leftIcon={Lock}
isPassword
value={form.confirmPassword}
onChangeText={text => handleInputChange('confirmPassword', text)}
/>
</AFormItem>
</AForm>
<AButton className="w-full mb-4" onPress={handleRegister} loading={loading}>
注册
</AButton>
</View>
</ScrollView>
</View>
)
}
@@ -1,19 +1,18 @@
import type { ReactNode } from 'react'
import type { DimensionValue, ViewStyle } from 'react-native'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import {
Animated,
Dimensions,
Modal,
Platform,
StyleSheet,
Text,
TouchableWithoutFeedback,
View,
} from 'react-native'
import React, { useEffect, useMemo, useState } from 'react'
import { Dimensions, StyleSheet, Text, View } from 'react-native'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, {
interpolate,
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import LucideIcon from './LucideIcon'
const WINDOW = Dimensions.get('window')
const SCREEN = Dimensions.get('screen')
interface DrawerMenuProps {
visible: boolean
@@ -27,218 +26,145 @@ interface DrawerMenuProps {
CircleNum?: number
isShadowHideModal?: boolean
title?: string
showHeader?: boolean
}
export default function Drawer({
visible,
height,
height = 300,
hideModal,
duration = 300,
isCircle,
menuPosition = 'bottom',
children,
CircleNum = 8,
CircleNum = 12,
isShadowHideModal = true,
header,
title,
showHeader = true,
}: DrawerMenuProps) {
const [menuPositionStyle, setMenuPositionStyle] = useState<ViewStyle>()
const [drawerMenuBox, setDrawerMenuBox] = useState<ViewStyle>()
const progress = useSharedValue(0)
const drag = useSharedValue(0)
const [innerVisible, setInnerVisible] = useState(false)
const anim = useRef(new Animated.Value(0)).current
/* ================= 显隐 ================= */
useEffect(() => {
if (visible) {
setInnerVisible(true)
open()
progress.value = withTiming(1, { duration })
}
else {
close()
progress.value = withTiming(0, { duration }, () => {
runOnJS(setInnerVisible)(false)
})
}
}, [visible])
useEffect(() => {
/* ================= Drawer 尺寸 ================= */
const drawerBox = useMemo<ViewStyle>(() => {
switch (menuPosition) {
case 'left':
setMenuPositionStyle({
flex: 1,
backgroundColor: '#fff',
borderTopRightRadius: isCircle ? CircleNum : 0,
borderBottomRightRadius: isCircle ? CircleNum : 0,
})
setDrawerMenuBox({
width: height,
height: WINDOW.height,
position: 'absolute',
left: 0,
top: 0,
})
break
return { width: height, left: 0, top: 0, bottom: 0 }
case 'right':
setMenuPositionStyle({
flex: 1,
backgroundColor: '#fff',
borderTopLeftRadius: isCircle ? CircleNum : 0,
borderBottomLeftRadius: isCircle ? CircleNum : 0,
})
setDrawerMenuBox({
width: height,
height: WINDOW.height,
position: 'absolute',
right: 0,
top: 0,
})
break
return { width: height, right: 0, top: 0, bottom: 0 }
case 'top':
setMenuPositionStyle({
flex: 1,
backgroundColor: '#fff',
borderBottomLeftRadius: isCircle ? CircleNum : 0,
borderBottomRightRadius: isCircle ? CircleNum : 0,
})
setDrawerMenuBox({
width: WINDOW.width,
height,
position: 'absolute',
top: 0,
right: 0,
})
break
return { height, left: 0, right: 0, top: 0 }
case 'bottom':
default:
setMenuPositionStyle({
flex: 1,
backgroundColor: '#fff',
borderTopLeftRadius: isCircle ? CircleNum : 0,
borderTopRightRadius: isCircle ? CircleNum : 0,
})
setDrawerMenuBox({
width: WINDOW.width,
height,
position: 'absolute',
bottom: 0,
right: 0,
})
break
return { height, left: 0, right: 0, bottom: 0 }
}
}, [menuPosition, height, isCircle, CircleNum])
}, [menuPosition, height])
/* ================== 动画 ================== */
const open = () => {
Animated.timing(anim, {
toValue: 1,
duration,
useNativeDriver: true,
}).start()
}
/* ================= 动画样式(关键) ================= */
const drawerStyle = useAnimatedStyle(() => {
switch (menuPosition) {
case 'bottom': {
const y = interpolate(progress.value, [0, 1], [SCREEN.height, 0], 'clamp')
return { transform: [{ translateY: y + drag.value }] }
}
case 'top': {
const y = interpolate(progress.value, [0, 1], [-SCREEN.height, 0], 'clamp')
return { transform: [{ translateY: y }] }
}
case 'left': {
const x = interpolate(progress.value, [0, 1], [-SCREEN.width, 0], 'clamp')
return { transform: [{ translateX: x }] }
}
case 'right': {
const x = interpolate(progress.value, [0, 1], [SCREEN.width, 0], 'clamp')
return { transform: [{ translateX: x }] }
}
default:
return {}
}
})
const close = () => {
Animated.timing(anim, {
toValue: 0,
duration,
useNativeDriver: true,
}).start(({ finished }) => {
if (finished) {
setInnerVisible(false)
hideModal()
const maskStyle = useAnimatedStyle(() => ({
opacity: interpolate(progress.value, [0, 1], [0, 0.45], 'clamp'),
}))
/* ================= 手势(只给 bottom) ================= */
const pan = Gesture.Pan()
.onUpdate((e) => {
if (menuPosition === 'bottom' && e.translationY > 0) {
drag.value = e.translationY
}
})
.onEnd(() => {
if (menuPosition === 'bottom' && drag.value > 120) {
progress.value = withTiming(0, { duration }, () => {
drag.value = 0
runOnJS(hideModal)()
})
}
else {
drag.value = withTiming(0)
}
})
}
/* ================== 位移 ================== */
const getTransform = useCallback(() => {
switch (menuPosition) {
case 'left':
return {
translateX: anim.interpolate({
inputRange: [0, 1],
outputRange: [-WINDOW.width, 0],
}),
}
case 'right':
return {
translateX: anim.interpolate({
inputRange: [0, 1],
outputRange: [WINDOW.width, 0],
}),
}
case 'top':
return {
translateY: anim.interpolate({
inputRange: [0, 1],
outputRange: [-WINDOW.height, 0],
}),
}
case 'bottom':
default:
return {
translateY: anim.interpolate({
inputRange: [0, 1],
outputRange: [WINDOW.height, 0],
}),
}
}
}, [menuPosition])
if (!innerVisible)
return null
return (
<Modal transparent visible animationType="none">
<View style={StyleSheet.absoluteFill} pointerEvents="box-none" className="z-[99]">
{/* 遮罩 */}
<TouchableWithoutFeedback onPress={() => isShadowHideModal && close()}>
<Animated.View
style={[
styles.mask,
{
opacity: anim.interpolate({
inputRange: [0, 1],
outputRange: [0, 0.5],
}),
},
]}
/>
</TouchableWithoutFeedback>
<Animated.View
style={[styles.mask, maskStyle]}
onTouchEnd={() => isShadowHideModal && hideModal()}
/>
{/* Drawer */}
<View style={drawerMenuBox} pointerEvents="box-none">
<Animated.View
pointerEvents="box-none"
style={[
styles.container,
{
transform: [getTransform()],
},
]}
>
<View style={menuPositionStyle}>
{menuPosition !== 'bottom' && (
<View style={{ paddingTop: Platform.OS === 'ios' ? 50 : 0 }} />
)}
{
header || (
<View style={[styles.drawerBox, drawerBox]}>
<GestureDetector gesture={pan}>
<Animated.View
style={[
styles.drawer,
drawerStyle,
{
borderTopLeftRadius: isCircle ? CircleNum : 0,
borderTopRightRadius: isCircle ? CircleNum : 0,
},
]}
>
{showHeader && (
header ?? (
<View style={styles.headTitle}>
<Text style={styles.title}>{title}</Text>
<LucideIcon
icon="X"
size={22}
color="black"
style={{ position: 'absolute', right: 15, top: 11 }}
onPress={close}
style={styles.close}
onPress={hideModal}
/>
</View>
)
}
)}
{children}
</View>
</Animated.View>
</Animated.View>
</GestureDetector>
</View>
</Modal>
</View>
)
}
@@ -247,18 +173,25 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFillObject,
backgroundColor: '#000',
},
container: {
height: '100%',
width: '100%',
drawerBox: {
position: 'absolute',
},
drawer: {
flex: 1,
backgroundColor: '#fff',
},
headTitle: {
width: '100%',
padding: 10,
justifyContent: 'center',
padding: 12,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 18,
fontWeight: '600',
},
close: {
position: 'absolute',
right: 16,
top: 12,
},
})
+2 -2
View File
@@ -30,8 +30,8 @@ function isWhiteListed(url: string) {
}
const alovaInstance = createAlova({
// baseURL: 'http://192.168.5.6:8086/app',
baseURL: 'http://192.168.1.137:8086/app',
baseURL: 'http://192.168.5.6:8086/app',
// baseURL: 'http://192.168.1.137:8086/app',
requestAdapter: axiosRequestAdapter(),
timeout: 5000,
cacheFor: null,
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

+1 -3
View File
@@ -26,21 +26,19 @@ export const useAppStore = create<AppState>((set, get) => ({
const moneyCategoryStore = useMoneyCategoryStore.getState()
const spaceStore = useSpaceStore.getState()
// 未登录:跳转到登录页,但认为初始化已完成(允许 UI 继续渲染)
if (!userStore.isLoggedIn()) {
navStore.navigateToAuth()
set({ ready: true })
return
}
// 登录状态:执行完整初始化流程
try {
// 分类初始化
await moneyCategoryStore.init()
// 获取空间信息
const res = await getUserSpaceApi()
spaceStore.setUserSpace(res.data)
spaceStore.setUserSpace(res.data)
}
catch (err) {
console.error('app init error:', err)
+39 -11
View File
@@ -1,21 +1,49 @@
import type { icons } from 'lucide-react-native'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { router } from 'expo-router'
// store/navigation.ts
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'
export interface NavItem {
label: string
path: string
icon: keyof typeof icons
}
interface NavState {
navMenus: NavItem[]
pathname: string
setPathname: (p: string) => void
navigateToAuth: () => void
setNavMenus: (menus: NavItem[]) => void
}
export const useNavStore = create<NavState>(set => ({
pathname: '/',
setPathname: (p: string) => set({ pathname: p }),
navigateToAuth: () => {
const pathname = useNavStore.getState().pathname
const DEFAULT_NAV_MENUS: NavItem[] = [
{ label: '记账', path: '/money', icon: 'CircleDollarSign' },
{ label: '恋爱记', path: '/love', icon: 'Heart' },
{ label: '聊天', path: '/chat', icon: 'MessageCircle' },
{ label: '个人中心', path: '/user', icon: 'CircleUserRound' },
]
if (!pathname.startsWith('/auth')) {
router.replace('/auth')
}
},
}))
export const useNavStore = create<NavState>()(
persist(
(set, get) => ({
pathname: '/',
navMenus: DEFAULT_NAV_MENUS,
setPathname: (p: string) => set({ pathname: p }),
navigateToAuth: () => {
const pathname = useNavStore.getState().pathname
if (!pathname.startsWith('/auth')) {
router.replace('/auth')
}
},
setNavMenus: navMenus => set({ navMenus }),
}),
{
name: 'nav-storage',
storage: createJSONStorage(() => AsyncStorage),
},
),
)