u
This commit is contained in:
1 parent
44ab5416a3
commit
1935906676
17 files changed
+322
-266
No files matched your search
@@ -146,7 +146,7 @@ export default function StaticTransactions({
|
||||
}}
|
||||
>
|
||||
<View className="relative w-10 h-10 rounded-full bg-gray-100 items-center justify-center">
|
||||
<MoneyCategoryIcon name={item.category_icon} size={20} color="#000" />
|
||||
<MoneyCategoryIcon name={item.category_icon} size={24} color="#000" />
|
||||
</View>
|
||||
|
||||
<View className="flex-1">
|
||||
|
||||
@@ -34,8 +34,7 @@ export default function Money() {
|
||||
record_time: searchParams.record_time,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
moneyRecordStore.setRecords(res.data.records, res.data.total_expense, res.data.total_income, res.data.total_balance, searchParams.record_time,
|
||||
)
|
||||
moneyRecordStore.setRecords(res.data.records, searchParams.record_time)
|
||||
}
|
||||
else {
|
||||
toast.show({
|
||||
@@ -143,7 +142,7 @@ export default function Money() {
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{moneyRecordStore.total_income}
|
||||
{moneyRecordStore.getTotalIncome()}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -154,7 +153,7 @@ export default function Money() {
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{moneyRecordStore.total_expense}
|
||||
{moneyRecordStore.getTotalExpense()}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -165,7 +164,7 @@ export default function Money() {
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{moneyRecordStore.total_balance}
|
||||
{moneyRecordStore.getTotalBalance()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
+56
-18
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Pressable, Text, View } from 'react-native'
|
||||
import { AButton } from '@/components/common/ao'
|
||||
import { AButton, AInput } from '@/components/common/ao'
|
||||
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
||||
import {
|
||||
Drawer,
|
||||
@@ -10,34 +10,38 @@ import {
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
} from '@/components/ui/drawer'
|
||||
import { Input, InputField } from '@/components/ui/input'
|
||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||
import { MoneyCategoryEnum } from '@/types/money/category'
|
||||
|
||||
interface CreateCategoryProps {
|
||||
export interface EditCategoryForm {
|
||||
id?: number
|
||||
name: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
interface EditCategoryProps {
|
||||
open: boolean
|
||||
onClose?: () => void
|
||||
type: MoneyCategoryEnum
|
||||
}
|
||||
|
||||
interface CreateCategoryForm {
|
||||
icon: string
|
||||
name: string
|
||||
onConfirm?: (form: EditCategoryForm) => Promise<void>
|
||||
}
|
||||
|
||||
export default function CreateCategory({
|
||||
open,
|
||||
onClose,
|
||||
type,
|
||||
}: CreateCategoryProps) {
|
||||
onConfirm,
|
||||
}: EditCategoryProps) {
|
||||
const moneyCategoryStore = useMoneyCategoryStore()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(open)
|
||||
|
||||
const [form, setForm] = useState<CreateCategoryForm>({
|
||||
const [form, setForm] = useState<EditCategoryForm>({
|
||||
icon: '',
|
||||
name: '',
|
||||
})
|
||||
|
||||
const [confirmDisabled, setConfirmDisabled] = useState(true)
|
||||
const [confirmLoading, setConfirmLoading] = useState(false)
|
||||
|
||||
@@ -46,13 +50,13 @@ export default function CreateCategory({
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
if (form.name && form.name.length < 6) {
|
||||
if (form.name && form.name.length <= 5 && form.icon) {
|
||||
setConfirmDisabled(false)
|
||||
}
|
||||
else {
|
||||
setConfirmDisabled(true)
|
||||
}
|
||||
}, [form.name])
|
||||
}, [form.name, form.icon])
|
||||
|
||||
function handleClose() {
|
||||
setIsOpen(false)
|
||||
@@ -79,15 +83,49 @@ export default function CreateCategory({
|
||||
|
||||
<Text className="text-lg font-medium">{type === MoneyCategoryEnum.Expense ? '添加支出分类' : '添加收入分类'}</Text>
|
||||
|
||||
<AButton size="xs" disabled={confirmDisabled} loading={confirmLoading}>确认</AButton>
|
||||
<AButton
|
||||
size="xs"
|
||||
disabled={confirmDisabled}
|
||||
loading={confirmLoading}
|
||||
onPress={async () => {
|
||||
if (!onConfirm) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setConfirmLoading(true)
|
||||
|
||||
const trimmedForm = {
|
||||
...form,
|
||||
name: form.name.trim(),
|
||||
}
|
||||
|
||||
await onConfirm(trimmedForm)
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
// 外部报错时,保持弹窗打开,loading 结束
|
||||
}
|
||||
finally {
|
||||
setConfirmLoading(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认
|
||||
</AButton>
|
||||
</View>
|
||||
|
||||
<Input
|
||||
variant="outline"
|
||||
>
|
||||
<InputField placeholder="请输入名称(不超过5个字符)" />
|
||||
</Input>
|
||||
|
||||
<AInput
|
||||
placeholder="请输入名称(不超过5个字符)"
|
||||
onChangeText={(text) => {
|
||||
setForm((val) => {
|
||||
return {
|
||||
...val,
|
||||
name: text,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</DrawerHeader>
|
||||
<DrawerBody className="mb-16">
|
||||
{Object.entries(moneyCategoryStore.ICON_GROUPS).map(([groupName, icons]) => (
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { SortableGridRenderItem } from 'react-native-sortables'
|
||||
import type { EditCategoryForm } from './components/EditCategory'
|
||||
import type { MoneyCategoryItem } from '@/types/money/category'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Pressable, Text, Vibration, View } from 'react-native'
|
||||
|
||||
import { Pressable, Text, Vibration, View } from 'react-native'
|
||||
import Animated, { useAnimatedRef } from 'react-native-reanimated'
|
||||
import Sortable from 'react-native-sortables'
|
||||
import { AButton, ATab, LucideIcon } from '@/components/common/ao'
|
||||
@@ -10,7 +11,8 @@ import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||
import { MoneyCategoryEnum } from '@/types/money/category'
|
||||
import CreateCategory from './components/CreateCategory'
|
||||
import { toast } from '@/utils/toast'
|
||||
import EditCategory from './components/EditCategory'
|
||||
|
||||
export default function MoneyCategorySetting() {
|
||||
const [selectedTab, setSelectedTab] = useState(MoneyCategoryEnum.Expense)
|
||||
@@ -27,7 +29,7 @@ export default function MoneyCategorySetting() {
|
||||
|
||||
useEffect(() => {
|
||||
setCategories(selectedTab === MoneyCategoryEnum.Expense ? moneyCategoryStore.getExpenseCategories() : moneyCategoryStore.getIncomeCategories())
|
||||
}, [selectedTab])
|
||||
}, [selectedTab, moneyCategoryStore.categories])
|
||||
|
||||
const renderItem = useCallback<SortableGridRenderItem<MoneyCategoryItem>>(
|
||||
({ item }) => (
|
||||
@@ -61,6 +63,42 @@ export default function MoneyCategorySetting() {
|
||||
[],
|
||||
)
|
||||
|
||||
async function handleConfirmEdit(form: EditCategoryForm) {
|
||||
const isDuplicate = moneyCategoryStore.isDuplicateCategory(form.name, selectedTab)
|
||||
|
||||
if (isDuplicate) {
|
||||
toast.show({
|
||||
type: 'warning',
|
||||
title: '类别不能重复',
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await moneyCategoryStore.createCategory({
|
||||
id: 0,
|
||||
name: form.name,
|
||||
icon: form.icon,
|
||||
sort_number: 0,
|
||||
type: selectedTab,
|
||||
})
|
||||
|
||||
setOpenCreateCategory(false)
|
||||
}
|
||||
catch (err) {
|
||||
const message
|
||||
= err instanceof Error
|
||||
? err.message
|
||||
: '新增失败,请稍后再试'
|
||||
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1">
|
||||
<CustomHeader
|
||||
@@ -146,7 +184,6 @@ export default function MoneyCategorySetting() {
|
||||
// 设置 1 秒后删除
|
||||
const timer = setTimeout(() => {
|
||||
const deleteId = Number(draggingId)
|
||||
setCategories(prev => prev.filter(c => c.id !== deleteId))
|
||||
|
||||
moneyCategoryStore.deleteCategoryById(deleteId)
|
||||
|
||||
@@ -189,10 +226,11 @@ export default function MoneyCategorySetting() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<CreateCategory
|
||||
<EditCategory
|
||||
type={selectedTab}
|
||||
open={openCreateCategory}
|
||||
onClose={() => setOpenCreateCategory(false)}
|
||||
onConfirm={handleConfirmEdit}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -83,6 +83,21 @@ export default function AmountKeyboard({
|
||||
})
|
||||
}, [record_time, amount, remark])
|
||||
|
||||
useEffect(() => {
|
||||
if (!keydownInput) {
|
||||
handleChange({ amount: '0' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const computed = calculateWithBig(keydownInput)
|
||||
handleChange({ amount: computed })
|
||||
}
|
||||
catch {
|
||||
handleChange({ amount: '0' })
|
||||
}
|
||||
}, [keydownInput])
|
||||
|
||||
const handleChange = (newData: Partial<typeof internalData>) => {
|
||||
const updated = { ...internalData, ...newData }
|
||||
setInternalData(updated)
|
||||
@@ -98,9 +113,11 @@ export default function AmountKeyboard({
|
||||
newInput = prev.slice(0, -1)
|
||||
}
|
||||
else if (key === 'clear') {
|
||||
const currentDate = getCurrentDate()
|
||||
handleChange({ record_time: currentDate, amount: '', remark: '' })
|
||||
|
||||
setInternalData({
|
||||
record_time: getCurrentDate(),
|
||||
amount: '0',
|
||||
remark: '',
|
||||
})
|
||||
return ''
|
||||
}
|
||||
else if (key === '/') {
|
||||
@@ -189,16 +206,7 @@ export default function AmountKeyboard({
|
||||
newInput = prev + keyStr
|
||||
}
|
||||
}
|
||||
|
||||
// 计算结果
|
||||
try {
|
||||
const computed = calculateWithBig(newInput)
|
||||
handleChange({ amount: computed })
|
||||
}
|
||||
catch (e) {
|
||||
handleChange({ amount: '0' })
|
||||
}
|
||||
|
||||
|
||||
return newInput
|
||||
})
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function CategoryGrid({
|
||||
>
|
||||
<MoneyCategoryIcon
|
||||
name={item.icon}
|
||||
size={24}
|
||||
size={28}
|
||||
color={
|
||||
isSelected ? '#fff' : '#000'
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ export default function MoenyRecordEditor() {
|
||||
if (res.isSuccess) {
|
||||
const result: MoneyRecordItem = {
|
||||
...res.data,
|
||||
category_id: selectedCategory.id,
|
||||
category_type: selectedCategory.type,
|
||||
category_name: selectedCategory.name,
|
||||
category_icon: selectedCategory.icon,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DeleteMoneyCategoryParams, QueryMoneyCategoryResult, UpdateMoneyCategorySortParams } from '@/types/money/category'
|
||||
import type { CreateMoneyCategoryParams, CreateMoneyCategoryResult, DeleteMoneyCategoryParams, QueryMoneyCategoryResult, UpdateMoneyCategorySortParams } from '@/types/money/category'
|
||||
import alovaInstance from '../../index'
|
||||
|
||||
const API_PREFIX = '/money/category'
|
||||
@@ -14,3 +14,7 @@ export function deleteMoneyCategoryApi(params: DeleteMoneyCategoryParams) {
|
||||
export function updateMoneyCategorySortApi(params: UpdateMoneyCategorySortParams) {
|
||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/updateSort`, params)
|
||||
}
|
||||
|
||||
export function createMoneyCategoryApi(params: CreateMoneyCategoryParams) {
|
||||
return alovaInstance.Post<ResponseData<CreateMoneyCategoryResult>>(`${API_PREFIX}/create`, params)
|
||||
}
|
||||
@@ -32,8 +32,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,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { MoneyCategoryItem, UpdateMoneyCategorySortItem } from '@/types/money/category'
|
||||
import { create } from 'zustand'
|
||||
import { deleteMoneyCategoryApi, queryMoneyCategoryApi, updateMoneyCategorySortApi } from '@/request/api/money/category'
|
||||
import { createMoneyCategoryApi, deleteMoneyCategoryApi, queryMoneyCategoryApi, updateMoneyCategorySortApi } from '@/request/api/money/category'
|
||||
import { MoneyCategoryEnum } from '@/types/money/category'
|
||||
|
||||
interface MoneyCategoryStore {
|
||||
ICON_GROUPS: Record<string, string[]>
|
||||
@@ -8,15 +9,17 @@ interface MoneyCategoryStore {
|
||||
|
||||
init: () => Promise<void>
|
||||
setCategories: (categories: MoneyCategoryItem[]) => void
|
||||
addCategory: (category: MoneyCategoryItem) => void
|
||||
|
||||
getExpenseCategories: () => MoneyCategoryItem[]
|
||||
getIncomeCategories: () => MoneyCategoryItem[]
|
||||
|
||||
getCategoryById: (id: number) => MoneyCategoryItem | null
|
||||
|
||||
updateSortNumbers: (data: MoneyCategoryItem[], type: number) => void
|
||||
deleteCategoryById: (id: number) => void
|
||||
createCategory: (category: MoneyCategoryItem) => Promise<void>
|
||||
updateSortNumbers: (data: MoneyCategoryItem[], type: number) => Promise<void>
|
||||
deleteCategoryById: (id: number) => Promise<void>
|
||||
|
||||
isDuplicateCategory: (name: string, type: number) => boolean
|
||||
|
||||
clear: () => void
|
||||
}
|
||||
@@ -94,9 +97,6 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
|
||||
setCategories: categories => set({ categories }),
|
||||
|
||||
addCategory: category =>
|
||||
set({ categories: [...get().categories, category] }),
|
||||
|
||||
init: async () => {
|
||||
try {
|
||||
await queryMoneyCategoryApi().then((res) => {
|
||||
@@ -118,63 +118,98 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
set({ categories: [] })
|
||||
},
|
||||
|
||||
getExpenseCategories: () => (get().categories || []).filter(c => c.type === 0),
|
||||
getIncomeCategories: () => (get().categories || []).filter(c => c.type === 1),
|
||||
getExpenseCategories: () =>
|
||||
(get().categories || [])
|
||||
.filter(c => c.type === MoneyCategoryEnum.Expense)
|
||||
.sort((a, b) => (a.sort_number ?? 0) - (b.sort_number ?? 0)),
|
||||
|
||||
getIncomeCategories: () =>
|
||||
(get().categories || [])
|
||||
.filter(c => c.type === MoneyCategoryEnum.Income)
|
||||
.sort((a, b) => (a.sort_number ?? 0) - (b.sort_number ?? 0)),
|
||||
|
||||
getCategoryById: (id: number) => {
|
||||
return get().categories.find(c => c.id === id) || null
|
||||
},
|
||||
|
||||
updateSortNumbers: (data, type) => {
|
||||
// 获取当前所有分类
|
||||
const allCategories = get().categories
|
||||
createCategory: async (category) => {
|
||||
const categories = category.type === MoneyCategoryEnum.Expense ? get().getExpenseCategories() : get().getIncomeCategories()
|
||||
|
||||
// 保留另一类分类
|
||||
const otherCategories = allCategories.filter(c => c.type !== type)
|
||||
const maxSortNumber = categories[categories.length - 1].sort_number + 1 || 1
|
||||
|
||||
// 给当前类型的数据重新计算 sort_number
|
||||
const updatedData = data.map((item, index) => ({
|
||||
...item,
|
||||
sort_number: index, // 按顺序重新赋值
|
||||
}))
|
||||
try {
|
||||
const res = await createMoneyCategoryApi({
|
||||
name: category.name,
|
||||
icon: category.icon,
|
||||
type: category.type,
|
||||
sort_number: maxSortNumber,
|
||||
})
|
||||
|
||||
// 合并另一类分类
|
||||
const finalCategories = [...otherCategories, ...updatedData]
|
||||
if (!res.isSuccess) {
|
||||
throw new Error(res.message)
|
||||
}
|
||||
|
||||
// 构造后端请求 payload,只需要 id 和 sort_number
|
||||
const payload: UpdateMoneyCategorySortItem[] = updatedData.map(item => ({
|
||||
category.id = res.data.id
|
||||
category.sort_number = maxSortNumber
|
||||
|
||||
set({
|
||||
categories: [...get().categories, category],
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
updateSortNumbers: async (data, type) => {
|
||||
const payload = data.map((item, index) => ({
|
||||
id: item.id,
|
||||
sort_number: item.sort_number,
|
||||
sort_number: index,
|
||||
}))
|
||||
|
||||
// 调用后端接口更新排序
|
||||
updateMoneyCategorySortApi({
|
||||
data: payload,
|
||||
}).then((res) => {
|
||||
console.log('后端返回:', res)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
try {
|
||||
const res = await updateMoneyCategorySortApi({ data: payload })
|
||||
if (!res.isSuccess) {
|
||||
throw new Error(res.message)
|
||||
}
|
||||
|
||||
// 更新前端 state
|
||||
set({ categories: finalCategories })
|
||||
// 更新前端 state
|
||||
const allCategories = get().categories
|
||||
const otherCategories = allCategories.filter(c => c.type !== type)
|
||||
const updatedData = data.map((item, index) => ({
|
||||
...item,
|
||||
sort_number: index,
|
||||
}))
|
||||
|
||||
set({ categories: [...otherCategories, ...updatedData] })
|
||||
}
|
||||
catch (err) {
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
deleteCategoryById: async (id: number) => {
|
||||
const allCategories = get().categories
|
||||
try {
|
||||
const res = await deleteMoneyCategoryApi({ id })
|
||||
if (!res.isSuccess) {
|
||||
throw new Error(res.message)
|
||||
}
|
||||
|
||||
// 删除指定 id,同时只处理同类型的数据
|
||||
const finalCategories = allCategories
|
||||
.filter(c => c.id !== id)
|
||||
// 更新 state
|
||||
const allCategories = get().categories
|
||||
const finalCategories = allCategories.filter(c => c.id !== id)
|
||||
set({ categories: finalCategories })
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
// 更新 state
|
||||
set({ categories: finalCategories })
|
||||
|
||||
// 调用删除接口
|
||||
await deleteMoneyCategoryApi({ id }).catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
isDuplicateCategory: (name, type) => {
|
||||
const categories = get().categories || []
|
||||
return categories.some(c => c.name === name && c.type === type)
|
||||
},
|
||||
}),
|
||||
|
||||
)
|
||||
@@ -1,19 +1,20 @@
|
||||
import type { MoneyRecordGroup, MoneyRecordItem } from '@/types/money/record'
|
||||
import { create } from 'zustand'
|
||||
import { deleteMoneyRecordApi } from '@/request/api/money/record'
|
||||
import { MoneyCategoryEnum } from '@/types/money/category'
|
||||
import { add, sub } from '@/utils/math'
|
||||
import { isInMonth } from '@/utils/time'
|
||||
import { useMoneyCategoryStore } from './moneyCategory'
|
||||
|
||||
interface MoneyRecordStore {
|
||||
records: MoneyRecordGroup[]
|
||||
currentMonth: string
|
||||
|
||||
total_income: string
|
||||
total_expense: string
|
||||
total_balance: string
|
||||
/* computed */
|
||||
getTotalIncome: () => string
|
||||
getTotalExpense: () => string
|
||||
getTotalBalance: () => string
|
||||
|
||||
setRecords: (records: MoneyRecordGroup[], total_expense: string, total_income: string, total_balance: string, month: string) => void
|
||||
setRecords: (records: MoneyRecordGroup[], month: string) => void
|
||||
setCurrentMonth: (month: string) => void
|
||||
|
||||
addMoneyRecord: (data: MoneyRecordItem) => void
|
||||
@@ -23,164 +24,77 @@ interface MoneyRecordStore {
|
||||
getMoneyRecordById: (id: number) => MoneyRecordItem | null
|
||||
}
|
||||
|
||||
function recomputeGroups(records: MoneyRecordItem[]) {
|
||||
const groupsMap: Record<string, MoneyRecordGroup> = {}
|
||||
|
||||
records.forEach((r) => {
|
||||
const group = groupsMap[r.record_time] ?? { record_time: r.record_time, records: [], income: '0.00', expense: '0.00' }
|
||||
group.records.push(r)
|
||||
if (r.category_type === MoneyCategoryEnum.Expense)
|
||||
group.expense = add(group.expense, r.amount)
|
||||
if (r.category_type === MoneyCategoryEnum.Income)
|
||||
group.income = add(group.income, r.amount)
|
||||
groupsMap[r.record_time] = group
|
||||
})
|
||||
|
||||
return Object.values(groupsMap).sort((a, b) => b.record_time.localeCompare(a.record_time))
|
||||
}
|
||||
|
||||
export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
records: [],
|
||||
currentMonth: '',
|
||||
|
||||
total_income: '0.00',
|
||||
total_expense: '0.00',
|
||||
total_balance: '0.00',
|
||||
/* ---------------- computed ---------------- */
|
||||
getTotalIncome: () => {
|
||||
return get().records.reduce((sum, g) => add(sum, g.income), '0.00')
|
||||
},
|
||||
|
||||
setRecords: (records, total_expense, total_income, total_balance, month) => {
|
||||
set({ records, currentMonth: month, total_income, total_expense, total_balance })
|
||||
getTotalExpense: () => {
|
||||
return get().records.reduce((sum, g) => add(sum, g.expense), '0.00')
|
||||
},
|
||||
|
||||
getTotalBalance: () => {
|
||||
return sub(get().getTotalIncome(), get().getTotalExpense())
|
||||
},
|
||||
|
||||
/* ---------------- base ---------------- */
|
||||
setRecords: (records, month) => {
|
||||
const flatRecords = records.flatMap(g => g.records)
|
||||
const newGroups = recomputeGroups(flatRecords)
|
||||
set({ records: newGroups, currentMonth: month })
|
||||
},
|
||||
|
||||
setCurrentMonth: month => set({ currentMonth: month }),
|
||||
|
||||
/* ---------------- actions ---------------- */
|
||||
addMoneyRecord: (data) => {
|
||||
set((prev) => {
|
||||
if (!isInMonth(data.record_time, prev.currentMonth))
|
||||
return prev
|
||||
|
||||
const category = useMoneyCategoryStore.getState().getById(data.category_id)
|
||||
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 (category?.type === 0)
|
||||
group.expense = add(group.expense, amount)
|
||||
if (category?.type === 1)
|
||||
group.income = add(group.income, amount)
|
||||
records[groupIndex] = group
|
||||
}
|
||||
else {
|
||||
const newGroup: MoneyRecordGroup = {
|
||||
record_time: data.record_time,
|
||||
records: [data],
|
||||
expense: category?.type === 0 ? amount : '0.00',
|
||||
income: 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)
|
||||
}
|
||||
|
||||
const totalExpense = category?.type === 0 ? add(prev.total_expense, amount) : prev.total_expense
|
||||
const totalIncome = category?.type === 1 ? add(prev.total_income, amount) : prev.total_income
|
||||
const totalBalance = sub(totalIncome, totalExpense)
|
||||
return {
|
||||
...prev,
|
||||
records,
|
||||
total_expense: totalExpense,
|
||||
total_income: totalIncome,
|
||||
total_balance: totalBalance,
|
||||
}
|
||||
})
|
||||
if (!isInMonth(data.record_time, get().currentMonth))
|
||||
return
|
||||
const flatRecords = [...get().records.flatMap(g => g.records), data]
|
||||
const newGroups = recomputeGroups(flatRecords)
|
||||
set({ records: newGroups })
|
||||
},
|
||||
|
||||
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 updateRecordCategory = useMoneyCategoryStore.getState().getById((updated.category_id))
|
||||
|
||||
const oldGroup = { ...records[oldGroupIndex], records: [...records[oldGroupIndex].records] }
|
||||
const oldRecordIndex = oldGroup.records.findIndex(r => r.id === updated.id)
|
||||
const oldRecord = oldGroup.records[oldRecordIndex]
|
||||
const oldRecordCategory = useMoneyCategoryStore.getState().getById((oldRecord.category_id))
|
||||
|
||||
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 (oldRecordCategory?.type === 0)
|
||||
total_expense = sub(total_expense, oldAmount)
|
||||
if (oldRecordCategory?.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 (updateRecordCategory?.type === 0)
|
||||
group.expense = add(group.expense, newAmount)
|
||||
if (updateRecordCategory?.type === 1)
|
||||
group.income = add(group.income, newAmount)
|
||||
records[newGroupIndex] = group
|
||||
}
|
||||
else {
|
||||
const newGroup: MoneyRecordGroup = {
|
||||
record_time: updated.record_time,
|
||||
records: [updated],
|
||||
expense: updateRecordCategory?.type === 0 ? newAmount : '0.00',
|
||||
income: updateRecordCategory?.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 (updateRecordCategory?.type === 0)
|
||||
total_expense = add(total_expense, newAmount)
|
||||
if (updateRecordCategory?.type === 1)
|
||||
total_income = add(total_income, newAmount)
|
||||
|
||||
const total_balance = sub(total_income, total_expense)
|
||||
|
||||
return { ...prev, records, total_expense, total_income, total_balance }
|
||||
})
|
||||
const flatRecords = get().records.flatMap(g =>
|
||||
g.records.map(r => r.id === updated.id ? updated : r),
|
||||
)
|
||||
// 如果更新后的记录不在当前月,则直接删除
|
||||
const filteredRecords = flatRecords.filter(r => isInMonth(r.record_time, get().currentMonth))
|
||||
const newGroups = recomputeGroups(filteredRecords)
|
||||
set({ records: newGroups })
|
||||
},
|
||||
|
||||
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]
|
||||
const category = useMoneyCategoryStore.getState().getById(record.category_id)
|
||||
|
||||
if (category?.id === 0)
|
||||
total_expense = sub(total_expense, record.amount)
|
||||
if (category?.id === 1)
|
||||
total_income = sub(total_income, record.amount)
|
||||
group.records.splice(index, 1)
|
||||
}
|
||||
})
|
||||
|
||||
const newRecords = records.filter(g => g.records.length > 0)
|
||||
|
||||
const total_balance = sub(total_income, total_expense)
|
||||
|
||||
return { ...prev, records: newRecords, total_expense, total_income, total_balance }
|
||||
})
|
||||
const flatRecords = get().records.flatMap(g => g.records).filter(r => r.id !== id)
|
||||
const newGroups = recomputeGroups(flatRecords)
|
||||
set({ records: newGroups })
|
||||
|
||||
deleteMoneyRecordApi({ id }).catch(console.error)
|
||||
},
|
||||
|
||||
getMoneyRecordById: (id: number) => {
|
||||
const records = get().records
|
||||
for (const group of records) {
|
||||
getMoneyRecordById: (id) => {
|
||||
for (const group of get().records) {
|
||||
const record = group.records.find(r => r.id === id)
|
||||
if (record)
|
||||
return record
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface CreateMoneyCategoryParams {
|
||||
name: string
|
||||
icon: string
|
||||
type: MoneyCategoryEnum
|
||||
sort_number: number
|
||||
}
|
||||
|
||||
export interface CreateMoneyCategoryResult {
|
||||
|
||||
Reference in new issue
Block a user