u
This commit is contained in:
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.
@@ -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,
|
||||
},
|
||||
})
|
||||
@@ -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 |
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
),
|
||||
)
|
||||
Reference in new issue
Block a user