u
This commit is contained in:
1 parent
49285f1df2
commit
63b168affc
7 files changed
+284
-324
No files matched your search
@@ -1,22 +1,21 @@
|
||||
import type { MoneyRecordGroup, MoneyRecordItem, QueryMoneyRecordsResult } from '@/types/money/record'
|
||||
import { useRouter } from 'expo-router'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Text, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native'
|
||||
import { Text, TouchableOpacity, View } from 'react-native'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { ADatePicker, LucideIcon } from '@/components/common/ao'
|
||||
import { Fab, FabIcon } from '@/components/ui/fab'
|
||||
import { AddIcon } from '@/components/ui/icon'
|
||||
import { Progress, ProgressFilledTrack } from '@/components/ui/progress'
|
||||
import { deleteMoneyRecordApi, queryMoneyRecordsApi } from '@/request/api/money/record'
|
||||
import { add, sub } from '@/utils/math'
|
||||
import { getCurrentYearMonth, isInMonth } from '@/utils/time'
|
||||
import { useMoneyRecordStore } from '@/store/moneyRecord'
|
||||
import { getCurrentYearMonth } from '@/utils/time'
|
||||
import { toast } from '@/utils/toast'
|
||||
import AddMoneyDrawer from './components/AddDrawer'
|
||||
import TransactionsList from './components/TransactionsList'
|
||||
|
||||
export default function Money() {
|
||||
const moneyRecordStore = useMoneyRecordStore()
|
||||
const router = useRouter()
|
||||
const [showAddDrawer, setShowAddDrawer] = useState(false)
|
||||
// 安全高度
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
@@ -25,22 +24,13 @@ export default function Money() {
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
record_time: getCurrentYearMonth(),
|
||||
})
|
||||
// 数据
|
||||
const [moneyRecords, setMoneyRecords] = useState<QueryMoneyRecordsResult>({
|
||||
total_expense: '0',
|
||||
total_income: '0',
|
||||
records: [],
|
||||
})
|
||||
// 更新
|
||||
const [updateItem, setUpdateItem] = useState<MoneyRecordItem | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
queryMoneyRecordsApi({
|
||||
record_time: searchParams.record_time,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
console.log(res.data.records)
|
||||
setMoneyRecords(res.data)
|
||||
moneyRecordStore.setRecords(res.data.records, res.data.total_expense, res.data.total_income, searchParams.record_time)
|
||||
}
|
||||
else {
|
||||
toast.show({
|
||||
@@ -52,232 +42,6 @@ export default function Money() {
|
||||
})
|
||||
}, [searchParams])
|
||||
|
||||
function handleAddMoneyRecord(data: MoneyRecordItem) {
|
||||
const recordTime = data.record_time
|
||||
|
||||
setMoneyRecords((prev) => {
|
||||
// 不在当前月份 → 不添加,直接返回
|
||||
if (!isInMonth(recordTime, searchParams.record_time)) {
|
||||
return prev
|
||||
}
|
||||
|
||||
const next = structuredClone(prev)
|
||||
|
||||
// 查找分组
|
||||
const group = next.records.find(g => g.record_time === recordTime)
|
||||
|
||||
// === 已存在分组 ===
|
||||
if (group) {
|
||||
// 插入到最前面
|
||||
group.records.unshift(data)
|
||||
|
||||
// 更新分组 & 总计
|
||||
if (data.category_type === 1) {
|
||||
// 收入
|
||||
group.income = add(group.income, data.amount)
|
||||
next.total_income = add(next.total_income, data.amount)
|
||||
}
|
||||
else if (data.category_type === 0) {
|
||||
// 支出
|
||||
group.expense = add(group.expense, data.amount)
|
||||
next.total_expense = add(next.total_expense, data.amount)
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
// === 无分组,新建 ===
|
||||
const expense = data.category_type === 0 ? data.amount : '0'
|
||||
const income = data.category_type === 1 ? data.amount : '0'
|
||||
|
||||
const newGroup: MoneyRecordGroup = {
|
||||
record_time: recordTime,
|
||||
income,
|
||||
expense,
|
||||
records: [data],
|
||||
}
|
||||
|
||||
// ---- 按日期(YYYY-MM-DD)倒序插入 ----
|
||||
// 最新日期插入前面,比如:2025-01-05 > 2025-01-03
|
||||
const index = next.records.findIndex(g => g.record_time < recordTime)
|
||||
|
||||
if (index === -1) {
|
||||
next.records.push(newGroup)
|
||||
}
|
||||
else {
|
||||
next.records.splice(index, 0, newGroup)
|
||||
}
|
||||
|
||||
// 更新总计
|
||||
if (data.category_type === 0) {
|
||||
next.total_income = add(next.total_income, data.amount)
|
||||
}
|
||||
else if (data.category_type === 1) {
|
||||
next.total_expense = add(next.total_expense, data.amount)
|
||||
}
|
||||
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function handleUpdateMoneyRecord(updated: MoneyRecordItem) {
|
||||
setMoneyRecords((prev) => {
|
||||
const next = structuredClone(prev)
|
||||
|
||||
// 找到旧记录
|
||||
const oldGroupIndex = next.records.findIndex(g =>
|
||||
g.records.some(r => r.id === updated.id),
|
||||
)
|
||||
if (oldGroupIndex === -1)
|
||||
return next
|
||||
|
||||
const oldGroup = next.records[oldGroupIndex]
|
||||
const oldRecordIndex = oldGroup.records.findIndex(r => r.id === updated.id)
|
||||
const oldRecord = oldGroup.records[oldRecordIndex]
|
||||
|
||||
const oldAmount = oldRecord.amount
|
||||
const newAmount = updated.amount
|
||||
const oldType = oldRecord.category_type
|
||||
const newType = updated.category_type
|
||||
const oldDate = oldRecord.record_time
|
||||
const newDate = updated.record_time
|
||||
|
||||
// record_time 没变 → 同分组更新
|
||||
if (oldDate === newDate) {
|
||||
oldGroup.records[oldRecordIndex] = updated
|
||||
|
||||
// 调整分组金额
|
||||
if (oldType === 1)
|
||||
oldGroup.income = sub(oldGroup.income, oldAmount)
|
||||
if (oldType === 0)
|
||||
oldGroup.expense = sub(oldGroup.expense, oldAmount)
|
||||
|
||||
if (newType === 1)
|
||||
oldGroup.income = add(oldGroup.income, newAmount)
|
||||
if (newType === 0)
|
||||
oldGroup.expense = add(oldGroup.expense, newAmount)
|
||||
|
||||
// 调整总计
|
||||
if (oldType === 1)
|
||||
next.total_income = sub(next.total_income, oldAmount)
|
||||
if (oldType === 0)
|
||||
next.total_expense = sub(next.total_expense, oldAmount)
|
||||
|
||||
if (newType === 1)
|
||||
next.total_income = add(next.total_income, newAmount)
|
||||
if (newType === 0)
|
||||
next.total_expense = add(next.total_expense, newAmount)
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// 判断是否在当前月份内
|
||||
// -------------------------
|
||||
const currentMonth = searchParams.record_time // 例如 "2025-01"
|
||||
|
||||
const isInCurrentMonth = isInMonth(newDate, currentMonth)
|
||||
|
||||
// 1. 先移除旧分组 & 调整金额(无论是否在当前月份,都要做)
|
||||
if (oldType === 1)
|
||||
oldGroup.income = sub(oldGroup.income, oldAmount)
|
||||
if (oldType === 0)
|
||||
oldGroup.expense = sub(oldGroup.expense, oldAmount)
|
||||
|
||||
if (oldType === 1)
|
||||
next.total_income = sub(next.total_income, oldAmount)
|
||||
if (oldType === 0)
|
||||
next.total_expense = sub(next.total_expense, oldAmount)
|
||||
|
||||
oldGroup.records.splice(oldRecordIndex, 1)
|
||||
if (oldGroup.records.length === 0) {
|
||||
next.records.splice(oldGroupIndex, 1)
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// 不在当前月份 → 不显示,直接返回 next
|
||||
// -------------------------
|
||||
if (!isInCurrentMonth) {
|
||||
return next
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// 在当前月份 → 移动到新分组
|
||||
// -------------------------
|
||||
|
||||
// 找新分组,如果没有就创建
|
||||
let newGroup = next.records.find(g => g.record_time === newDate)
|
||||
if (!newGroup) {
|
||||
newGroup = {
|
||||
record_time: newDate,
|
||||
income: '0.00',
|
||||
expense: '0.00',
|
||||
records: [],
|
||||
}
|
||||
// 按日期倒序插入
|
||||
const insertIndex = next.records.findIndex(g => g.record_time < newDate)
|
||||
if (insertIndex === -1)
|
||||
next.records.push(newGroup)
|
||||
else
|
||||
next.records.splice(insertIndex, 0, newGroup)
|
||||
}
|
||||
|
||||
// 添加到新分组
|
||||
newGroup.records.unshift(updated)
|
||||
|
||||
// 加金额
|
||||
if (newType === 1)
|
||||
newGroup.income = add(newGroup.income, newAmount)
|
||||
if (newType === 0)
|
||||
newGroup.expense = add(newGroup.expense, newAmount)
|
||||
|
||||
if (newType === 1)
|
||||
next.total_income = add(next.total_income, newAmount)
|
||||
if (newType === 0)
|
||||
next.total_expense = add(next.total_expense, newAmount)
|
||||
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function handleDeleteRecord(id: number) {
|
||||
// 直接更新本地状态
|
||||
setMoneyRecords((prev) => {
|
||||
const next = structuredClone(prev)
|
||||
|
||||
// 遍历每个分组
|
||||
next.records.forEach((group) => {
|
||||
const index = group.records.findIndex(r => r.id === id)
|
||||
if (index !== -1) {
|
||||
const record = group.records[index]
|
||||
|
||||
// 更新分组金额
|
||||
if (record.category_type === 1) {
|
||||
group.income = sub(group.income, record.amount)
|
||||
next.total_income = sub(next.total_income, record.amount)
|
||||
}
|
||||
else if (record.category_type === 0) {
|
||||
group.expense = sub(group.expense, record.amount)
|
||||
next.total_expense = sub(next.total_expense, record.amount)
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
group.records.splice(index, 1)
|
||||
}
|
||||
})
|
||||
|
||||
// 删除空分组
|
||||
next.records = next.records.filter(g => g.records.length > 0)
|
||||
|
||||
return next
|
||||
})
|
||||
|
||||
// 异步请求删除 API
|
||||
deleteMoneyRecordApi({ id }).catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<View
|
||||
@@ -307,19 +71,7 @@ export default function Money() {
|
||||
</View>
|
||||
|
||||
<View className="mt-3 m-4 ">
|
||||
|
||||
<View className="flex flex-row gap-4">
|
||||
<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"
|
||||
>
|
||||
0.00
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex-1">
|
||||
<Text className="text-gray-500 text-sm">收入</Text>
|
||||
<Text
|
||||
@@ -327,7 +79,7 @@ export default function Money() {
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
0.00
|
||||
{moneyRecordStore.total_income}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -338,7 +90,7 @@ export default function Money() {
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
0.00
|
||||
{moneyRecordStore.total_expense}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -374,13 +126,15 @@ export default function Money() {
|
||||
|
||||
<View className="flex-1 mt-2">
|
||||
<TransactionsList
|
||||
records={moneyRecords.records}
|
||||
records={moneyRecordStore.records}
|
||||
onUpdate={(data) => {
|
||||
setUpdateItem(data)
|
||||
setShowAddDrawer(true)
|
||||
router.navigate({
|
||||
pathname: '/money/record/editor',
|
||||
params: { id: data.id },
|
||||
})
|
||||
}}
|
||||
onDelete={(id) => {
|
||||
handleDeleteRecord(id)
|
||||
moneyRecordStore.deleteMoneyRecord(id)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -388,35 +142,16 @@ export default function Money() {
|
||||
<Fab
|
||||
size="lg"
|
||||
onPress={() => {
|
||||
// setShowAddDrawer(true)
|
||||
router.navigate('/money/record/editor')
|
||||
}}
|
||||
>
|
||||
<FabIcon as={AddIcon} size="xl" />
|
||||
</Fab>
|
||||
|
||||
<AddMoneyDrawer
|
||||
isOpen={showAddDrawer}
|
||||
updateData={updateItem}
|
||||
onClose={() => {
|
||||
setShowAddDrawer(false)
|
||||
setUpdateItem(null)
|
||||
}}
|
||||
onConfirm={(res) => {
|
||||
setShowAddDrawer(false)
|
||||
if (updateItem !== null) {
|
||||
setUpdateItem(null)
|
||||
handleUpdateMoneyRecord(res)
|
||||
}
|
||||
else {
|
||||
handleAddMoneyRecord(res)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ADatePicker
|
||||
show={showDatePicker}
|
||||
mode="month"
|
||||
date={searchParams.record_time}
|
||||
onConfirm={(val) => {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
import type { MoneyCategoryItem } from '@/types/money/category'
|
||||
import type { MoneyRecordBase, MoneyRecordItem } from '@/types/money/record'
|
||||
import { useRouter } from 'expo-router'
|
||||
import { useState } from 'react'
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Text, TouchableOpacity, View } from 'react-native'
|
||||
import { LucideIcon } from '@/components/common/ao'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
import PagerTabs from '@/components/custom/PagerTabs'
|
||||
import { insertMoneyRecordApi } from '@/request/api/money/record'
|
||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||
import { useMoneyRecordStore } from '@/store/moneyRecord'
|
||||
import { getCurrentDate, toDateString } from '@/utils/time'
|
||||
import { toast } from '@/utils/toast'
|
||||
import AmountKeyboard from './components/AmountKeyboard'
|
||||
import CategoryGrid from './components/CategoryGrid'
|
||||
|
||||
export default function MoenyRecordEditor() {
|
||||
const router = useRouter()
|
||||
|
||||
const moneyCategoryStore = useMoneyCategoryStore()
|
||||
const expense = useMoneyCategoryStore(state => state.expense)
|
||||
const income = useMoneyCategoryStore(state => state.income)
|
||||
const moneyRecordStore = useMoneyRecordStore()
|
||||
|
||||
console.log(moneyCategoryStore.categories)
|
||||
console.log(expense)
|
||||
console.log(income)
|
||||
const router = useRouter()
|
||||
const expense = moneyCategoryStore.getExpenseCategories()
|
||||
const income = moneyCategoryStore.getIncomeCategories()
|
||||
|
||||
const { id: updateId } = useLocalSearchParams<{ id: string }>()
|
||||
|
||||
// 选择顺序:0 支出 1收入
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
// 自定义键盘
|
||||
const [showKeydown, setShowKeydown] = useState(false)
|
||||
// 选中分类
|
||||
@@ -42,6 +41,25 @@ export default function MoenyRecordEditor() {
|
||||
|
||||
const [moneyRecord, setMoneyRecord] = useState<MoneyRecordBase>(defaultMoneyRecord)
|
||||
|
||||
useEffect(() => {
|
||||
if (updateId) {
|
||||
const updateData = moneyRecordStore.getMoneyRecordById(Number(updateId))
|
||||
if (updateData !== null) {
|
||||
setMoneyRecord({
|
||||
...updateData,
|
||||
record_time: toDateString(updateData.record_time),
|
||||
})
|
||||
setShowKeydown(true)
|
||||
|
||||
const moneyCategory = moneyCategoryStore.getCategoryById(updateData.category_id)
|
||||
|
||||
if (moneyCategory != null) {
|
||||
setSelectedCategory(moneyCategory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 保存记录
|
||||
function handleSaveRecord() {
|
||||
if (selectedCategory === null) {
|
||||
@@ -55,7 +73,39 @@ export default function MoenyRecordEditor() {
|
||||
toast.show({
|
||||
title: '请输入金额',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const amount = moneyRecord.amount.replace(/^-+/, '')
|
||||
|
||||
insertMoneyRecordApi({
|
||||
...moneyRecord,
|
||||
amount,
|
||||
category_id: selectedCategory.id,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.isSuccess) {
|
||||
const result: MoneyRecordItem = {
|
||||
...res.data,
|
||||
category_name: selectedCategory.name,
|
||||
category_icon: selectedCategory.icon,
|
||||
category_type: selectedCategory.type,
|
||||
}
|
||||
|
||||
moneyRecordStore.addMoneyRecord(result)
|
||||
|
||||
router.back()
|
||||
}
|
||||
else {
|
||||
toast.show({
|
||||
title: res.message || '保存失败',
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('发生异常啦')
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -70,6 +120,7 @@ export default function MoenyRecordEditor() {
|
||||
/>
|
||||
|
||||
<PagerTabs
|
||||
selectedIndex={selectedCategory?.type}
|
||||
left={(
|
||||
<TouchableOpacity className="w-[30%] flex items-center px-4 flex-row">
|
||||
<LucideIcon icon="LayoutList" />
|
||||
@@ -83,7 +134,7 @@ export default function MoenyRecordEditor() {
|
||||
router.back()
|
||||
}}
|
||||
>
|
||||
<Text className="ml-auto text-gray-400">取消</Text>
|
||||
<Text className="ml-auto text-base text-gray-400">取消</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
routes={[
|
||||
|
||||
@@ -13,7 +13,7 @@ interface PagerTabsProps {
|
||||
routes: PagerRoute[]
|
||||
left?: React.ReactNode
|
||||
right?: React.ReactNode
|
||||
initialIndex?: number
|
||||
selectedIndex?: number // 外部控制选中页
|
||||
onChange?: (key: string) => void
|
||||
indicatorWidth?: number
|
||||
|
||||
@@ -25,17 +25,24 @@ export default function PagerTabs({
|
||||
routes,
|
||||
left,
|
||||
right,
|
||||
initialIndex = 0,
|
||||
selectedIndex,
|
||||
onChange,
|
||||
indicatorWidth = 38,
|
||||
className,
|
||||
style,
|
||||
}: PagerTabsProps) {
|
||||
const pagerRef = React.useRef<PagerView>(null)
|
||||
const [index, setIndex] = React.useState(initialIndex)
|
||||
const [index, setIndex] = React.useState(selectedIndex ?? 0)
|
||||
const [tabWidth, setTabWidth] = React.useState(0)
|
||||
const scrollX = React.useRef(new Animated.Value(selectedIndex ?? 0)).current
|
||||
|
||||
const scrollX = React.useRef(new Animated.Value(0)).current
|
||||
// 外部 selectedIndex 更新时同步内部 index
|
||||
React.useEffect(() => {
|
||||
if (typeof selectedIndex === 'number' && selectedIndex !== index) {
|
||||
setIndex(selectedIndex)
|
||||
pagerRef.current?.setPage(selectedIndex)
|
||||
}
|
||||
}, [selectedIndex])
|
||||
|
||||
const onTabPress = (i: number) => {
|
||||
pagerRef.current?.setPage(i)
|
||||
@@ -54,7 +61,6 @@ export default function PagerTabs({
|
||||
}
|
||||
|
||||
const tabWidthPerTab = tabWidth / routes.length
|
||||
|
||||
const indicatorTranslateX = scrollX.interpolate({
|
||||
inputRange: routes.map((_, i) => i),
|
||||
outputRange: routes.map(
|
||||
@@ -66,9 +72,7 @@ export default function PagerTabs({
|
||||
return (
|
||||
<View
|
||||
className={`${className} flex-1`}
|
||||
style={[
|
||||
style,
|
||||
]}
|
||||
style={[style]}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="flex flex-row items-center">
|
||||
@@ -86,7 +90,7 @@ export default function PagerTabs({
|
||||
{routes.map((r, i) => (
|
||||
<TouchableOpacity
|
||||
key={r.key}
|
||||
className="flex-1 items-center justify-center py-1"
|
||||
className="flex-1 items-center justify-center py-2"
|
||||
onPress={() => onTabPress(i)}
|
||||
>
|
||||
<Text
|
||||
@@ -115,7 +119,7 @@ export default function PagerTabs({
|
||||
<PagerView
|
||||
ref={pagerRef}
|
||||
style={{ flex: 1 }}
|
||||
initialPage={initialIndex}
|
||||
initialPage={selectedIndex ?? 0}
|
||||
onPageScroll={onPageScroll}
|
||||
onPageSelected={e => onPageSelected(e.nativeEvent.position)}
|
||||
>
|
||||
|
||||
@@ -30,8 +30,8 @@ function isWhiteListed(url: string) {
|
||||
}
|
||||
|
||||
const alovaInstance = createAlova({
|
||||
// baseURL: 'http://192.168.5.6:8085/app',
|
||||
baseURL: 'http://192.168.1.136:8085/app',
|
||||
baseURL: 'http://192.168.5.6:8085/app',
|
||||
// baseURL: 'http://192.168.1.136:8085/app',
|
||||
requestAdapter: axiosRequestAdapter(),
|
||||
timeout: 5000,
|
||||
cacheFor: null,
|
||||
|
||||
@@ -7,14 +7,15 @@ import { queryMoneyCategoryApi } from '@/request/api/money/category'
|
||||
interface MoneyCategoryStore {
|
||||
categories: MoneyCategoryItem[]
|
||||
|
||||
// 通过 getter 动态计算
|
||||
expense: MoneyCategoryItem[]
|
||||
income: MoneyCategoryItem[]
|
||||
|
||||
setCategories: (categories: MoneyCategoryItem[]) => void
|
||||
addCategory: (category: MoneyCategoryItem) => void
|
||||
removeCategory: (id: number) => void
|
||||
init: () => Promise<void>
|
||||
|
||||
getExpenseCategories: () => MoneyCategoryItem[]
|
||||
getIncomeCategories: () => MoneyCategoryItem[]
|
||||
|
||||
getCategoryById: (id: number) => MoneyCategoryItem | null
|
||||
}
|
||||
|
||||
export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
@@ -22,34 +23,32 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
(set, get) => ({
|
||||
categories: [],
|
||||
|
||||
// getter 样式属性,通过 get() 动态计算
|
||||
get expense() {
|
||||
return get().categories.filter(c => c.type === 0)
|
||||
},
|
||||
get income() {
|
||||
return get().categories.filter(c => c.type === 1)
|
||||
},
|
||||
|
||||
setCategories: categories => set({ categories }),
|
||||
|
||||
addCategory: category =>
|
||||
set(state => ({ categories: [...state.categories, category] })),
|
||||
set({ categories: [...get().categories, category] }),
|
||||
|
||||
removeCategory: id =>
|
||||
set(state => ({ categories: state.categories.filter(c => c.id !== id) })),
|
||||
set({ categories: get().categories.filter(c => c.id !== id) }),
|
||||
|
||||
init: async () => {
|
||||
try {
|
||||
const res = await queryMoneyCategoryApi()
|
||||
if (res.isSuccess) {
|
||||
const categories = res.data || []
|
||||
set({ categories })
|
||||
set({ categories: res.data || [] })
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch categories:', error)
|
||||
catch (e) {
|
||||
console.error('Fetch category failed:', e)
|
||||
}
|
||||
},
|
||||
|
||||
getExpenseCategories: () => (get().categories || []).filter(c => c.type === 0),
|
||||
getIncomeCategories: () => (get().categories || []).filter(c => c.type === 1),
|
||||
|
||||
getCategoryById: (id: number) => {
|
||||
return get().categories.find(c => c.id === id) || null
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'money-category-storage',
|
||||
|
||||
@@ -1,3 +1,172 @@
|
||||
import type { MoneyRecordGroup, MoneyRecordItem } from '@/types/money/record'
|
||||
import { create } from 'zustand'
|
||||
import { MoneyCategoryItem } from '@/types/money/category'
|
||||
import { MoneyRecordGroup, MoneyRecordItem } from '@/types/money/record'
|
||||
import { deleteMoneyRecordApi } from '@/request/api/money/record'
|
||||
import { add, sub } from '@/utils/math'
|
||||
import { isInMonth } from '@/utils/time'
|
||||
|
||||
interface MoneyRecordStore {
|
||||
records: MoneyRecordGroup[]
|
||||
currentMonth: string
|
||||
|
||||
total_income: string
|
||||
total_expense: string
|
||||
|
||||
setRecords: (records: MoneyRecordGroup[], total_expense: string, total_income: string, month: string) => void
|
||||
setCurrentMonth: (month: string) => void
|
||||
|
||||
addMoneyRecord: (data: MoneyRecordItem) => void
|
||||
updateMoneyRecord: (updated: MoneyRecordItem) => void
|
||||
deleteMoneyRecord: (id: number) => void
|
||||
|
||||
getMoneyRecordById: (id: number) => MoneyRecordItem | null
|
||||
}
|
||||
|
||||
export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
records: [],
|
||||
currentMonth: '',
|
||||
|
||||
total_income: '0.00',
|
||||
total_expense: '0.00',
|
||||
|
||||
setRecords: (records, total_expense, total_income, month) => {
|
||||
set({ records, currentMonth: month, total_income, total_expense })
|
||||
},
|
||||
|
||||
setCurrentMonth: month => set({ currentMonth: month }),
|
||||
|
||||
addMoneyRecord: (data) => {
|
||||
set((prev) => {
|
||||
if (!isInMonth(data.record_time, prev.currentMonth))
|
||||
return prev
|
||||
|
||||
const records = [...prev.records]
|
||||
const groupIndex = records.findIndex(g => g.record_time === data.record_time)
|
||||
const amount = data.amount
|
||||
|
||||
if (groupIndex !== -1) {
|
||||
const group = { ...records[groupIndex], records: [data, ...records[groupIndex].records] }
|
||||
if (data.category_type === 0)
|
||||
group.expense = add(group.expense, amount)
|
||||
if (data.category_type === 1)
|
||||
group.income = add(group.income, amount)
|
||||
records[groupIndex] = group
|
||||
}
|
||||
else {
|
||||
const newGroup: MoneyRecordGroup = {
|
||||
record_time: data.record_time,
|
||||
records: [data],
|
||||
expense: data.category_type === 0 ? amount : '0.00',
|
||||
income: data.category_type === 1 ? amount : '0.00',
|
||||
}
|
||||
const insertIndex = records.findIndex(g => g.record_time < data.record_time)
|
||||
if (insertIndex === -1)
|
||||
records.push(newGroup)
|
||||
else records.splice(insertIndex, 0, newGroup)
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
records,
|
||||
total_expense: data.category_type === 0 ? add(prev.total_expense, amount) : prev.total_expense,
|
||||
total_income: data.category_type === 1 ? add(prev.total_income, amount) : prev.total_income,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
updateMoneyRecord: (updated) => {
|
||||
set((prev) => {
|
||||
const records = [...prev.records]
|
||||
const oldGroupIndex = records.findIndex(g => g.records.some(r => r.id === updated.id))
|
||||
if (oldGroupIndex === -1)
|
||||
return prev
|
||||
|
||||
const oldGroup = { ...records[oldGroupIndex], records: [...records[oldGroupIndex].records] }
|
||||
const oldRecordIndex = oldGroup.records.findIndex(r => r.id === updated.id)
|
||||
const oldRecord = oldGroup.records[oldRecordIndex]
|
||||
|
||||
const oldAmount = oldRecord.amount
|
||||
const newAmount = updated.amount
|
||||
|
||||
// 移除旧记录
|
||||
oldGroup.records.splice(oldRecordIndex, 1)
|
||||
let total_expense = prev.total_expense
|
||||
let total_income = prev.total_income
|
||||
if (oldRecord.category_type === 0)
|
||||
total_expense = sub(total_expense, oldAmount)
|
||||
if (oldRecord.category_type === 1)
|
||||
total_income = sub(total_income, oldAmount)
|
||||
if (oldGroup.records.length === 0)
|
||||
records.splice(oldGroupIndex, 1)
|
||||
else records[oldGroupIndex] = oldGroup
|
||||
|
||||
if (!isInMonth(updated.record_time, prev.currentMonth)) {
|
||||
return { ...prev, records, total_expense, total_income }
|
||||
}
|
||||
|
||||
// 添加到新分组
|
||||
const newGroupIndex = records.findIndex(g => g.record_time === updated.record_time)
|
||||
if (newGroupIndex !== -1) {
|
||||
const group = { ...records[newGroupIndex], records: [updated, ...records[newGroupIndex].records] }
|
||||
if (updated.category_type === 0)
|
||||
group.expense = add(group.expense, newAmount)
|
||||
if (updated.category_type === 1)
|
||||
group.income = add(group.income, newAmount)
|
||||
records[newGroupIndex] = group
|
||||
}
|
||||
else {
|
||||
const newGroup: MoneyRecordGroup = {
|
||||
record_time: updated.record_time,
|
||||
records: [updated],
|
||||
expense: updated.category_type === 0 ? newAmount : '0.00',
|
||||
income: updated.category_type === 1 ? newAmount : '0.00',
|
||||
}
|
||||
const insertIndex = records.findIndex(g => g.record_time < updated.record_time)
|
||||
if (insertIndex === -1)
|
||||
records.push(newGroup)
|
||||
else records.splice(insertIndex, 0, newGroup)
|
||||
}
|
||||
|
||||
if (updated.category_type === 0)
|
||||
total_expense = add(total_expense, newAmount)
|
||||
if (updated.category_type === 1)
|
||||
total_income = add(total_income, newAmount)
|
||||
|
||||
return { ...prev, records, total_expense, total_income }
|
||||
})
|
||||
},
|
||||
|
||||
deleteMoneyRecord: (id) => {
|
||||
set((prev) => {
|
||||
const records = prev.records.map(g => ({ ...g, records: [...g.records] }))
|
||||
let total_expense = prev.total_expense
|
||||
let total_income = prev.total_income
|
||||
|
||||
records.forEach((group) => {
|
||||
const index = group.records.findIndex(r => r.id === id)
|
||||
if (index !== -1) {
|
||||
const record = group.records[index]
|
||||
if (record.category_type === 0)
|
||||
total_expense = sub(total_expense, record.amount)
|
||||
if (record.category_type === 1)
|
||||
total_income = sub(total_income, record.amount)
|
||||
group.records.splice(index, 1)
|
||||
}
|
||||
})
|
||||
|
||||
const newRecords = records.filter(g => g.records.length > 0)
|
||||
return { ...prev, records: newRecords, total_expense, total_income }
|
||||
})
|
||||
|
||||
deleteMoneyRecordApi({ id }).catch(console.error)
|
||||
},
|
||||
|
||||
getMoneyRecordById: (id: number) => {
|
||||
const records = get().records
|
||||
for (const group of records) {
|
||||
const record = group.records.find(r => r.id === id)
|
||||
if (record)
|
||||
return record
|
||||
}
|
||||
return null
|
||||
},
|
||||
}))
|
||||
Reference in new issue
Block a user