This commit is contained in:
oneao committed 2026-03-05 22:24:22 +08:00
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>
)
}
+7 -20
View File
@@ -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>
)
+1 -1
View File
@@ -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)
// 选中分类
@@ -37,6 +37,7 @@ interface DateMorePickerProps {
endTime: string
}) => void
hiddenModes?: DateMorePickerMode[]
oneMode?: DateMorePickerMode
onChange?: (mode: DateMorePickerMode) => void
}
@@ -108,6 +109,7 @@ export default function DateMorePicker({
onClose,
onConfirm,
onChange,
oneMode,
hiddenModes = [],
}: DateMorePickerProps) {
const [localVisible, setLocalVisible] = useState(false)
@@ -249,6 +251,13 @@ export default function DateMorePicker({
]
const visibleTabItems = tabItems.filter((item) => {
if (oneMode) {
if (oneMode === item.key) {
return true
}
return false
}
if (item.key === 'unlimited' && isHidden('unlimited'))
return false
if (item.key === 'range' && isHidden('range'))
@@ -271,6 +280,12 @@ export default function DateMorePicker({
]
const visibleCoreModeOptions = coreModeOptions.filter((item) => {
if (oneMode) {
if (oneMode === item.mode) {
return true
}
return false
}
if (item.mode === 'date' && isHidden('date'))
return false
if (item.mode === 'month' && isHidden('month'))
@@ -293,9 +308,6 @@ export default function DateMorePicker({
const canConfirm = (() => {
if (internalMode !== 'range')
return true
console.log(internalMode)
console.log(internalStart)
console.log(internalEnd)
const startValid = dayjs(internalStart).isValid()
const endValid = dayjs(internalEnd).isValid()
@@ -323,7 +335,7 @@ export default function DateMorePicker({
</View>
{/* 模式选择 */}
{!['unlimited', 'range'].includes(internalMode) && (
{!oneMode && !['unlimited', 'range'].includes(internalMode) && (
<View className="mb-2">
<Text className="text-lg mb-1">选择模式</Text>
{visibleCoreModeOptions.length > 0 && (
@@ -1,5 +1,4 @@
import type { IDatePickerProps } from '@yz1311/react-native-wheel-picker'
import SegmentedControl from '@react-native-segmented-control/segmented-control'
import { DatePicker } from '@yz1311/react-native-wheel-picker'
import dayjs from 'dayjs'
import React, { useEffect, useRef, useState } from 'react'
@@ -33,6 +32,7 @@ export default function ADatePicker({
date,
show = false,
onClose,
mode = 'date',
onConfirm,
format,
...restProps
@@ -40,8 +40,6 @@ export default function ADatePicker({
const fadeAnim = useRef(new Animated.Value(0)).current
const slideAnim = useRef(new Animated.Value(300)).current
const [visible, setVisible] = useState(show)
const [index, setIndex] = useState(0)
const [mode, setMode] = useState<DatePickerMode>('month')
// 使用传入 format 或默认 format
const finalFormat = format || defaultFormatMap[mode]
@@ -83,58 +81,6 @@ export default function ADatePicker({
<Animated.View
style={[styles.pickerContainer, { transform: [{ translateY: slideAnim }] }]}
>
<View className="px-3 pt-2 flex flex-col gap-2">
<View className="w-full">
<Text>快速选择</Text>
{/* <ATab
data={[
{
label: '自定义',
key: '自定义',
},
{
label: '不限',
key: '不限',
},
{
label: '今天',
key: '今天',
},
{
label: '本月',
key: '本月',
},
{
label: '本年',
key: '本年',
},
{
label: '范围',
key: '范围',
},
]}
onChange={(key) => {
setMode('datetime')
}}
/> */}
</View>
<View className="w-full">
<Text>选择模式</Text>
<SegmentedControl
values={['日', '月', '年', '时间', '日期']}
selectedIndex={index}
style={{ borderRadius: 2, width: '100%' }}
sliderStyle={{
borderRadius: 2,
}}
onChange={(event: any) => setIndex(event.nativeEvent.selectedSegmentIndex)}
/>
</View>
</View>
<DatePicker
{...restProps}
date={toDate(date)}
@@ -142,16 +88,10 @@ export default function ADatePicker({
isModal={false}
onPickerCancel={handleCancel}
onPickerConfirm={handleConfirm}
showHeader={false}
onDateChange={(value) => {
console.log(value)
handleConfirm(value)
}}
/>
<View className="flex flex-row gap-4 px-3 pb-1 v">
{/* <AButton variant="outline" className="flex-1">取消</AButton>
<AButton className="flex-1">确认</AButton> */}
</View>
</Animated.View>
</Modal>
)
@@ -18,8 +18,8 @@ export interface FinanceRecord {
export interface ListFinanceRecordsParams {
start_time: string
end_time: string
category_id: number
category_type: number
category_id: number | null
category_type: number | null
}
export interface ListFinanceRecordsResult {