u
This commit is contained in:
1 parent
173eb2dd37
commit
26ff2381b7
17 files changed
+596
-305
No files matched your search
@@ -1,11 +1,11 @@
|
||||
import type { DateMorePickerMode } from '@/components/common/ao/DateMorePicker'
|
||||
import type { FinanceCategory } from '@/types/finance/category'
|
||||
import { useFocusEffect, useRouter } from 'expo-router'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Image, Text, TouchableOpacity, View } from 'react-native'
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { toast } from 'sonner-native'
|
||||
import { ADateMorePicker, ADatePicker, ADrawer, AListItem, LucideIcon } from '@/components/common/ao'
|
||||
import { ADateMorePicker, ADrawer, AListItem, LucideIcon } from '@/components/common/ao'
|
||||
import { resolveDateRange } from '@/components/common/ao/DateMorePicker'
|
||||
import { Fab, FabIcon } from '@/components/ui/fab'
|
||||
import { AddIcon } from '@/components/ui/icon'
|
||||
@@ -45,18 +45,32 @@ export default function Money() {
|
||||
function fetchFinanceRecords() {
|
||||
const resolveDate = resolveDateRange(dateMode, searchParams.startTime)
|
||||
|
||||
let searchType = 0
|
||||
let searchType: number | null = 0
|
||||
let searchId: number | null = 0
|
||||
|
||||
if (searchParams.category) {
|
||||
if (searchParams.category.id === -1 || searchParams.category.id === -2) {
|
||||
if (searchParams.category.id === 0) {
|
||||
searchId = null
|
||||
searchType = null
|
||||
}
|
||||
else if (searchParams.category.id === -1 || searchParams.category.id === -2) {
|
||||
searchId = null
|
||||
searchType = searchParams.category.type
|
||||
}
|
||||
else {
|
||||
searchId = searchParams.category.id
|
||||
searchType = null
|
||||
}
|
||||
}
|
||||
else {
|
||||
searchId = null
|
||||
searchType = null
|
||||
}
|
||||
|
||||
listFinanceRecordsApi({
|
||||
start_time: resolveDate.startTime,
|
||||
end_time: resolveDate.endTime,
|
||||
category_id: searchParams.category ? searchParams.category.id : 0,
|
||||
category_id: searchId,
|
||||
category_type: searchType,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
@@ -92,7 +106,7 @@ export default function Money() {
|
||||
|
||||
function timeModeTip() {
|
||||
if (dateMode === 'unlimited') {
|
||||
return '全部'
|
||||
return '全部时间'
|
||||
}
|
||||
else if (dateMode === 'range') {
|
||||
return `${searchParams.startTime} - ${searchParams.endTime}`
|
||||
@@ -135,21 +149,21 @@ export default function Money() {
|
||||
>
|
||||
<Text className="font-semibold text-lg">
|
||||
{
|
||||
searchParams.category ? searchParams.category.name : '分类'
|
||||
searchParams.category ? searchParams.category.name : '全部分类'
|
||||
}
|
||||
</Text>
|
||||
<LucideIcon icon="ChevronDown" size={16} color="#666" clickable={false} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
{/* <TouchableOpacity
|
||||
className="flex flex-row items-center gap-2"
|
||||
onPress={() => {
|
||||
setShowActionsMenu(true)
|
||||
}}
|
||||
>
|
||||
<LucideIcon icon="Menu" size={24} clickable={false} />
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity> */}
|
||||
</View>
|
||||
|
||||
<View
|
||||
@@ -249,21 +263,6 @@ export default function Money() {
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
{/* <ADatePicker
|
||||
show={showDatePicker}
|
||||
mode="month"
|
||||
date={searchParams.record_time}
|
||||
onConfirm={(val) => {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
record_time: val,
|
||||
})
|
||||
setShowDatePicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowDatePicker(false)
|
||||
}}
|
||||
/> */}
|
||||
|
||||
<CategorySheet
|
||||
open={showCategorySheet}
|
||||
@@ -280,7 +279,7 @@ export default function Money() {
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
|
||||
{/*
|
||||
<ADrawer
|
||||
visible={showActionsMenu}
|
||||
hideModal={() => {
|
||||
@@ -328,7 +327,7 @@ export default function Money() {
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</ADrawer>
|
||||
</ADrawer> */}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -2,9 +2,10 @@ import { useRouter } from 'expo-router'
|
||||
import { Image, Text, View } from 'react-native'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { AButton } from '@/components/common/ao'
|
||||
import { useNavStore } from '@/store/navigation'
|
||||
|
||||
export default function Index() {
|
||||
const router = useRouter()
|
||||
const navStore = useNavStore()
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-white">
|
||||
@@ -24,28 +25,14 @@ export default function Index() {
|
||||
记录生活,记录情绪
|
||||
</Text>
|
||||
|
||||
{/* <AButton
|
||||
<AButton
|
||||
className="w-full"
|
||||
onPress={() => ('/finance')}
|
||||
onPress={() => {
|
||||
navStore.navigateTo('finance/index')
|
||||
}}
|
||||
>
|
||||
开始使用
|
||||
</AButton> */}
|
||||
<View className="flex flex-row gap-3 mt-4">
|
||||
<AButton
|
||||
className="flex-1"
|
||||
onPress={() => ('/finance')}
|
||||
>
|
||||
创建空间
|
||||
</AButton>
|
||||
<AButton
|
||||
className="flex-1"
|
||||
variant="outline"
|
||||
onPress={() => ('/finance')}
|
||||
>
|
||||
加入空间
|
||||
</AButton>
|
||||
</View>
|
||||
|
||||
</AButton>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function NotFound() {
|
||||
title="(´・ω・`) 页面走丢了~"
|
||||
description="回首页看看吧!"
|
||||
footer={(
|
||||
<AButton onPress={() => router.replace('/')}>
|
||||
<AButton onPress={() => router.navigate('/finance')}>
|
||||
返回应用首页
|
||||
</AButton>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import type { AnalysisMoneyAmount } from '@/types/money/analysis'
|
||||
import dayjs from 'dayjs'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ScrollView, Text, TouchableOpacity, View } from 'react-native'
|
||||
import { VictoryChart, VictoryLabel, VictoryLine, VictoryPie, VictoryTheme, VictoryTooltip } from 'victory-native'
|
||||
import { ADatePicker, ADateRangePicker, ASegmentedTabs, LucideIcon } from '@/components/common/ao'
|
||||
import EmptyState from '@/components/common/EmptyStatus'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
import PagerTabs from '@/components/custom/PagerTabs'
|
||||
import { analysisMoneyLineApi, analysisMoneyPieApi } from '@/request/api/money/analysis'
|
||||
import { MoneyCategoryEnum } from '@/types/money/category'
|
||||
import { add } from '@/utils/math'
|
||||
import { getCurrentYear, getCurrentYearMonth, getEndOfMonth, getStartOfMonth } from '@/utils/time'
|
||||
|
||||
interface AnalysisContentProps {
|
||||
analysisType: MoneyCategoryEnum
|
||||
}
|
||||
|
||||
export default function Analysis() {
|
||||
function AnalysisContent({ analysisType }: AnalysisContentProps) {
|
||||
// 按月0 按年1 自定义2
|
||||
const [timeType, setTimeType] = useState<number>(0)
|
||||
|
||||
// 开始结束时间
|
||||
const [startDate, setStartDate] = useState(getCurrentYearMonth())
|
||||
const [endDate, setEndDate] = useState('')
|
||||
|
||||
// 显示
|
||||
const [showDatePicker, setShowDatePicker] = useState(false)
|
||||
|
||||
// 数据
|
||||
const [lineRecords, setLineRecords] = useState<AnalysisMoneyAmount[]>([])
|
||||
const [pieRecords, setPieRecords] = useState<AnalysisMoneyAmount[]>([])
|
||||
|
||||
// 请求
|
||||
function fetchChartsData() {
|
||||
let start = ''
|
||||
let end = ''
|
||||
|
||||
const selectedStartDate = dayjs(startDate)
|
||||
const selectedEndDate = dayjs(endDate)
|
||||
|
||||
let periodType = 0
|
||||
|
||||
if (timeType === 0) {
|
||||
start = selectedStartDate.startOf('month').format('YYYY-MM-DD')
|
||||
end = selectedStartDate.endOf('month').format('YYYY-MM-DD')
|
||||
|
||||
periodType = 0
|
||||
}
|
||||
else if (timeType === 1) {
|
||||
start = selectedStartDate.startOf('year').format('YYYY-MM-DD')
|
||||
end = selectedStartDate.endOf('year').format('YYYY-MM-DD')
|
||||
|
||||
periodType = 1
|
||||
}
|
||||
else {
|
||||
if (selectedStartDate.isSame(selectedEndDate, 'month')) {
|
||||
periodType = 0
|
||||
start = selectedStartDate.startOf('month').format('YYYY-MM-DD')
|
||||
end = selectedEndDate.endOf('month').format('YYYY-MM-DD')
|
||||
}
|
||||
else if (selectedStartDate.isSame(selectedEndDate, 'year')) {
|
||||
periodType = 1
|
||||
start = selectedStartDate.startOf('year').format('YYYY-MM-DD')
|
||||
end = selectedEndDate.endOf('year').format('YYYY-MM-DD')
|
||||
}
|
||||
else {
|
||||
periodType = 2
|
||||
start = selectedStartDate.format('YYYY-MM-DD')
|
||||
end = selectedEndDate.format('YYYY-MM-DD')
|
||||
}
|
||||
}
|
||||
|
||||
analysisMoneyLineApi({
|
||||
start_date: start,
|
||||
end_date: end,
|
||||
type: analysisType,
|
||||
period_type: periodType,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
setLineRecords(res.data.records)
|
||||
}
|
||||
})
|
||||
|
||||
analysisMoneyPieApi({
|
||||
start_date: start,
|
||||
end_date: end,
|
||||
type: analysisType,
|
||||
period_type: periodType,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
setPieRecords(res.data.records)
|
||||
}
|
||||
})
|
||||
}
|
||||
const totalAmount: number = pieRecords.reduce(
|
||||
(sum, item) => Number(add(sum, Number(item.y) || 0)),
|
||||
0,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
fetchChartsData()
|
||||
}, [timeType, startDate, endDate])
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
className="p-4"
|
||||
>
|
||||
<ASegmentedTabs
|
||||
value={timeType}
|
||||
onChange={(index) => {
|
||||
setTimeType(index)
|
||||
if (index === 2) {
|
||||
// 自定义类型,本月的 1号 - 终止号
|
||||
setStartDate(getStartOfMonth())
|
||||
setEndDate(getEndOfMonth())
|
||||
}
|
||||
else if (index === 1) {
|
||||
// 按年
|
||||
setStartDate(getCurrentYear())
|
||||
setEndDate('')
|
||||
}
|
||||
else {
|
||||
// 按月
|
||||
setStartDate(getCurrentYearMonth())
|
||||
setEndDate('')
|
||||
}
|
||||
}}
|
||||
tabs={[
|
||||
{ key: '1', label: '按月' },
|
||||
{ key: '2', label: '按年' },
|
||||
{ key: '3', label: '自定义' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
className="flex flex-row justify-center items-center"
|
||||
>
|
||||
<LucideIcon icon="CalendarDays" size={14} color="#666" clickable={false} />
|
||||
<Text className="font-semibold text-lg mr-2 ml-1">{startDate}</Text>
|
||||
{timeType === 2 && (
|
||||
<>
|
||||
<Text> - </Text>
|
||||
<Text className="font-semibold text-lg mx-1">
|
||||
{endDate}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<LucideIcon icon="ChevronDown" size={16} color="#666" clickable={false} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View className="mt-4">
|
||||
<Text className="text-lg font-semibold text-gray-700">趋势分析</Text>
|
||||
|
||||
<VictoryChart
|
||||
height={220}
|
||||
theme={VictoryTheme.material}
|
||||
>
|
||||
<VictoryLine
|
||||
interpolation="catmullRom"
|
||||
data={lineRecords}
|
||||
animate={{
|
||||
duration: 1000,
|
||||
}}
|
||||
/>
|
||||
</VictoryChart>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className="mt-4"
|
||||
>
|
||||
<Text className="text-lg font-semibold text-gray-700">分类构成</Text>
|
||||
|
||||
{
|
||||
pieRecords.length > 0
|
||||
? (
|
||||
<VictoryPie
|
||||
height={250}
|
||||
data={pieRecords}
|
||||
theme={VictoryTheme.material}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<EmptyState title="空" />
|
||||
)
|
||||
}
|
||||
|
||||
</View>
|
||||
|
||||
<View className="mt-4" style={{ paddingBottom: 24 }}>
|
||||
<View className="flex flex-row justify-between">
|
||||
<Text className="text-lg font-semibold text-gray-700">分类详细</Text>
|
||||
<Text>
|
||||
总:
|
||||
{totalAmount}
|
||||
{' '}
|
||||
元
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{
|
||||
pieRecords.length > 0
|
||||
? (
|
||||
<View className="mt-4 flex flex-col gap-2">
|
||||
{pieRecords.map((item, index) => (
|
||||
<View
|
||||
key={index}
|
||||
className="flex flex-row justify-between items-center py-3 border-b border-gray-300"
|
||||
>
|
||||
<Text className="text-base font-medium text-gray-800">{item.x}</Text>
|
||||
<Text className="text-base font-semibold text-gray-700">
|
||||
{item.y}
|
||||
{' '}
|
||||
元
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
<EmptyState title="空" />
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
</View>
|
||||
|
||||
{
|
||||
timeType === 2
|
||||
? (
|
||||
<ADateRangePicker
|
||||
show={showDatePicker}
|
||||
onConfirm={(startDate, endDate) => {
|
||||
setStartDate(startDate)
|
||||
setEndDate(endDate)
|
||||
setShowDatePicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowDatePicker(false)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ADatePicker
|
||||
show={showDatePicker}
|
||||
mode={timeType === 0 ? 'month' : 'year'}
|
||||
onConfirm={(val) => {
|
||||
setStartDate(val)
|
||||
setShowDatePicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowDatePicker(false)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1">
|
||||
<CustomHeader
|
||||
showHeader={false}
|
||||
/>
|
||||
|
||||
<PagerTabs
|
||||
showCustomHeader={true}
|
||||
routes={[
|
||||
{
|
||||
key: 'expense',
|
||||
title: '支出',
|
||||
element: <AnalysisContent analysisType={MoneyCategoryEnum.Expense} />,
|
||||
},
|
||||
{
|
||||
key: 'income',
|
||||
title: '收入',
|
||||
element: <AnalysisContent analysisType={MoneyCategoryEnum.Income} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { DateMorePickerMode } from '@/components/common/ao/DateMorePicker'
|
||||
import type { FinanceRecord } from '@/types/finance/record'
|
||||
import Big from 'big.js'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Keyboard, Pressable, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'
|
||||
import { ADateMorePicker } from '@/components/common/ao'
|
||||
import { ADatePicker, AInput, LucideIcon } from '@/components/common/ao/index'
|
||||
import { useKeyboardHeight } from '@/hooks/useKeyboardHeight'
|
||||
import { getCurrentDate } from '@/utils/time'
|
||||
@@ -51,10 +53,11 @@ export default function AmountKeyboard({
|
||||
record,
|
||||
onDone,
|
||||
}: AmountKeyboardProps) {
|
||||
// 日期
|
||||
const [showDatePicker, setShowDatePicker] = useState(false)
|
||||
const [dateMode, setDateMode] = useState<DateMorePickerMode>('date')
|
||||
// 自定义键盘计算过程
|
||||
const [keydownInput, setKeydownInput] = useState('')
|
||||
// 日期选择
|
||||
const [showDatePicker, setShowDatePicker] = useState(false)
|
||||
// 备注聚焦
|
||||
const [remarkIsFocused, setRemarkIsFocused] = useState(false)
|
||||
// 键盘高度
|
||||
@@ -342,7 +345,7 @@ export default function AmountKeyboard({
|
||||
<View style={{ height: keyboardHeight + 10 - 220 }} />
|
||||
)}
|
||||
|
||||
<ADatePicker
|
||||
{/* <ADatePicker
|
||||
pickerTitle="记账时间"
|
||||
mode="date"
|
||||
date={innerRecord.record_time}
|
||||
@@ -358,6 +361,24 @@ export default function AmountKeyboard({
|
||||
record_time: date,
|
||||
})
|
||||
}}
|
||||
/> */}
|
||||
|
||||
<ADateMorePicker
|
||||
visible={showDatePicker}
|
||||
mode={dateMode}
|
||||
onClose={() => {
|
||||
setShowDatePicker(false)
|
||||
}}
|
||||
startDate={innerRecord.record_time}
|
||||
oneMode="date"
|
||||
onConfirm={(value) => {
|
||||
setDateMode(value.mode)
|
||||
|
||||
setInnerRecord(val => ({
|
||||
...val,
|
||||
record_time: value.startTime,
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useLocalSearchParams, useRouter } from 'expo-router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Text, TouchableOpacity, View } from 'react-native'
|
||||
import { toast } from 'sonner-native'
|
||||
import { LucideIcon } from '@/components/common/ao'
|
||||
import { ADateMorePicker, LucideIcon } from '@/components/common/ao'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
import PagerTabs from '@/components/custom/PagerTabs'
|
||||
import { createFinanceRecordApi, updateFinanceRecordApi } from '@/request/api/finance/record'
|
||||
@@ -21,6 +21,7 @@ export default function MoenyRecordEditor() {
|
||||
const income = financeStore.getIncomeCategories()
|
||||
|
||||
const { id: updateId } = useLocalSearchParams<{ id: string }>()
|
||||
|
||||
// 自定义键盘
|
||||
const [showKeydown, setShowKeydown] = useState(false)
|
||||
// 选中分类
|
||||
|
||||
Reference in new issue
Block a user