u
This commit is contained in:
1 parent
44ab5416a3
commit
1935906676
17 files changed
+315
-259
No files matched your search
@@ -10,6 +10,7 @@ import (
|
|||||||
"allapp/utils/idgen"
|
"allapp/utils/idgen"
|
||||||
"allapp/utils/response"
|
"allapp/utils/response"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
money "allapp/biz/model/money"
|
money "allapp/biz/model/money"
|
||||||
|
|
||||||
@@ -123,8 +124,11 @@ func UpdateMoneyCategorySort(ctx context.Context, c *app.RequestContext) {
|
|||||||
func CreateMoneyCategory(ctx context.Context, c *app.RequestContext) {
|
func CreateMoneyCategory(ctx context.Context, c *app.RequestContext) {
|
||||||
var err error
|
var err error
|
||||||
var req money.CreateMoneyCategoryReq
|
var req money.CreateMoneyCategoryReq
|
||||||
|
|
||||||
err = c.BindAndValidate(&req)
|
err = c.BindAndValidate(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
|
||||||
c.String(consts.StatusBadRequest, err.Error())
|
c.String(consts.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -139,8 +143,7 @@ func CreateMoneyCategory(ctx context.Context, c *app.RequestContext) {
|
|||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Icon: req.Icon,
|
Icon: req.Icon,
|
||||||
Type: req.Type,
|
Type: req.Type,
|
||||||
// 默认排序号为ID
|
SortNumber: req.SortNumber,
|
||||||
SortNumber: 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插入
|
// 插入
|
||||||
|
|||||||
@@ -423,9 +423,10 @@ type CreateMoneyCategoryReq struct {
|
|||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
Name string `protobuf:"bytes,1,opt,name=Name,proto3" form:"name" json:"name" vd:"$ != '' && len($) <= 5"`
|
Name string `protobuf:"bytes,1,opt,name=Name,proto3" form:"name" json:"name" vd:"$ != ''"`
|
||||||
Icon string `protobuf:"bytes,2,opt,name=Icon,proto3" form:"icon" json:"icon" vd:"$ != ''"`
|
Icon string `protobuf:"bytes,2,opt,name=Icon,proto3" form:"icon" json:"icon" vd:"$ != ''"`
|
||||||
Type int32 `protobuf:"varint,3,opt,name=Type,proto3" form:"type" json:"type"`
|
Type int32 `protobuf:"varint,3,opt,name=Type,proto3" form:"type" json:"type"`
|
||||||
|
SortNumber int32 `protobuf:"varint,4,opt,name=SortNumber,proto3" form:"sort_number" json:"sort_number"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CreateMoneyCategoryReq) Reset() {
|
func (x *CreateMoneyCategoryReq) Reset() {
|
||||||
@@ -481,6 +482,13 @@ func (x *CreateMoneyCategoryReq) GetType() int32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *CreateMoneyCategoryReq) GetSortNumber() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.SortNumber
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type CreateMoneyCategoryResp struct {
|
type CreateMoneyCategoryResp struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -572,22 +580,24 @@ var file_money_money_category_types_proto_rawDesc = []byte{
|
|||||||
0x74, 0x65, 0x6d, 0x42, 0x08, 0xca, 0xbb, 0x18, 0x04, 0x64, 0x61, 0x74, 0x61, 0x52, 0x04, 0x44,
|
0x74, 0x65, 0x6d, 0x42, 0x08, 0xca, 0xbb, 0x18, 0x04, 0x64, 0x61, 0x74, 0x61, 0x52, 0x04, 0x44,
|
||||||
0x61, 0x74, 0x61, 0x22, 0x1d, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x6f, 0x6e,
|
0x61, 0x74, 0x61, 0x22, 0x1d, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x6f, 0x6e,
|
||||||
0x65, 0x79, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x53, 0x6f, 0x72, 0x74, 0x52, 0x65,
|
0x65, 0x79, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x53, 0x6f, 0x72, 0x74, 0x52, 0x65,
|
||||||
0x73, 0x70, 0x22, 0x97, 0x01, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x6f, 0x6e,
|
0x73, 0x70, 0x22, 0xb9, 0x01, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x6f, 0x6e,
|
||||||
0x65, 0x79, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x12, 0x36, 0x0a,
|
0x65, 0x79, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x12, 0x27, 0x0a,
|
||||||
0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x22, 0xca, 0xbb, 0x18,
|
0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x13, 0xca, 0xbb, 0x18,
|
||||||
0x04, 0x6e, 0x61, 0x6d, 0x65, 0xda, 0xbb, 0x18, 0x16, 0x24, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27,
|
0x04, 0x6e, 0x61, 0x6d, 0x65, 0xda, 0xbb, 0x18, 0x07, 0x24, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27,
|
||||||
0x20, 0x26, 0x26, 0x20, 0x6c, 0x65, 0x6e, 0x28, 0x24, 0x29, 0x20, 0x3c, 0x3d, 0x20, 0x35, 0x52,
|
0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x04, 0x49, 0x63, 0x6f, 0x6e, 0x18, 0x02,
|
||||||
0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x04, 0x49, 0x63, 0x6f, 0x6e, 0x18, 0x02, 0x20,
|
0x20, 0x01, 0x28, 0x09, 0x42, 0x13, 0xca, 0xbb, 0x18, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0xda, 0xbb,
|
||||||
0x01, 0x28, 0x09, 0x42, 0x13, 0xca, 0xbb, 0x18, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0xda, 0xbb, 0x18,
|
0x18, 0x07, 0x24, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x52, 0x04, 0x49, 0x63, 0x6f, 0x6e, 0x12,
|
||||||
0x07, 0x24, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x52, 0x04, 0x49, 0x63, 0x6f, 0x6e, 0x12, 0x1c,
|
0x1c, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x42, 0x08, 0xca,
|
||||||
0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x42, 0x08, 0xca, 0xbb,
|
0xbb, 0x18, 0x04, 0x74, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2f, 0x0a,
|
||||||
0x18, 0x04, 0x74, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x22, 0x31, 0x0a, 0x17,
|
0x0a, 0x53, 0x6f, 0x72, 0x74, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
|
||||||
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x6f, 0x6e, 0x65, 0x79, 0x43, 0x61, 0x74, 0x65, 0x67,
|
0x05, 0x42, 0x0f, 0xca, 0xbb, 0x18, 0x0b, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6e, 0x75, 0x6d, 0x62,
|
||||||
0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x12, 0x16, 0x0a, 0x02, 0x49, 0x64, 0x18, 0x01, 0x20,
|
0x65, 0x72, 0x52, 0x0a, 0x53, 0x6f, 0x72, 0x74, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x31,
|
||||||
0x01, 0x28, 0x03, 0x42, 0x06, 0xca, 0xbb, 0x18, 0x02, 0x69, 0x64, 0x52, 0x02, 0x49, 0x64, 0x42,
|
0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x6f, 0x6e, 0x65, 0x79, 0x43, 0x61, 0x74,
|
||||||
0x18, 0x5a, 0x16, 0x61, 0x6c, 0x6c, 0x61, 0x70, 0x70, 0x2f, 0x62, 0x69, 0x7a, 0x2f, 0x6d, 0x6f,
|
0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x12, 0x16, 0x0a, 0x02, 0x49, 0x64, 0x18,
|
||||||
0x64, 0x65, 0x6c, 0x2f, 0x6d, 0x6f, 0x6e, 0x65, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x06, 0xca, 0xbb, 0x18, 0x02, 0x69, 0x64, 0x52, 0x02, 0x49,
|
||||||
0x33,
|
0x64, 0x42, 0x18, 0x5a, 0x16, 0x61, 0x6c, 0x6c, 0x61, 0x70, 0x70, 0x2f, 0x62, 0x69, 0x7a, 0x2f,
|
||||||
|
0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2f, 0x6d, 0x6f, 0x6e, 0x65, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||||
|
0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ INSERT INTO money_record (
|
|||||||
user_id,
|
user_id,
|
||||||
amount,
|
amount,
|
||||||
remark,
|
remark,
|
||||||
record_time
|
record_time,
|
||||||
|
create_time,
|
||||||
|
update_time
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5,$6
|
$1, $2, $3, $4, $5,$6,NOW(),NOW()
|
||||||
)
|
)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
|
|||||||
@@ -181,9 +181,11 @@ INSERT INTO money_record (
|
|||||||
user_id,
|
user_id,
|
||||||
amount,
|
amount,
|
||||||
remark,
|
remark,
|
||||||
record_time
|
record_time,
|
||||||
|
create_time,
|
||||||
|
update_time
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5,$6
|
$1, $2, $3, $4, $5,$6,NOW(),NOW()
|
||||||
)
|
)
|
||||||
RETURNING id, category_id, user_id, amount, remark, record_time, create_time, update_time
|
RETURNING id, category_id, user_id, amount, remark, record_time, create_time, update_time
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -47,13 +47,14 @@ message UpdateMoneyCategorySortResp {}
|
|||||||
message CreateMoneyCategoryReq {
|
message CreateMoneyCategoryReq {
|
||||||
string Name = 1 [
|
string Name = 1 [
|
||||||
(api.body)="name",
|
(api.body)="name",
|
||||||
(api.vd) = "$ != '' && len($) <= 5"
|
(api.vd) = "$ != ''"
|
||||||
];
|
];
|
||||||
string Icon = 2 [
|
string Icon = 2 [
|
||||||
(api.body)="icon",
|
(api.body)="icon",
|
||||||
(api.vd) = "$ != ''"
|
(api.vd) = "$ != ''"
|
||||||
];
|
];
|
||||||
int32 Type = 3 [(api.body)="type"];
|
int32 Type = 3 [(api.body)="type"];
|
||||||
|
int32 SortNumber = 4 [(api.body)="sort_number"];
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateMoneyCategoryResp {
|
message CreateMoneyCategoryResp {
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export default function StaticTransactions({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View className="relative w-10 h-10 rounded-full bg-gray-100 items-center justify-center">
|
<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>
|
||||||
|
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ export default function Money() {
|
|||||||
record_time: searchParams.record_time,
|
record_time: searchParams.record_time,
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if (res.isSuccess) {
|
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 {
|
else {
|
||||||
toast.show({
|
toast.show({
|
||||||
@@ -143,7 +142,7 @@ export default function Money() {
|
|||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
ellipsizeMode="tail"
|
ellipsizeMode="tail"
|
||||||
>
|
>
|
||||||
{moneyRecordStore.total_income}
|
{moneyRecordStore.getTotalIncome()}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -154,7 +153,7 @@ export default function Money() {
|
|||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
ellipsizeMode="tail"
|
ellipsizeMode="tail"
|
||||||
>
|
>
|
||||||
{moneyRecordStore.total_expense}
|
{moneyRecordStore.getTotalExpense()}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -165,7 +164,7 @@ export default function Money() {
|
|||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
ellipsizeMode="tail"
|
ellipsizeMode="tail"
|
||||||
>
|
>
|
||||||
{moneyRecordStore.total_balance}
|
{moneyRecordStore.getTotalBalance()}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
+56
-18
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Pressable, Text, View } from 'react-native'
|
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 MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
@@ -10,34 +10,38 @@ import {
|
|||||||
DrawerFooter,
|
DrawerFooter,
|
||||||
DrawerHeader,
|
DrawerHeader,
|
||||||
} from '@/components/ui/drawer'
|
} from '@/components/ui/drawer'
|
||||||
import { Input, InputField } from '@/components/ui/input'
|
|
||||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||||
import { MoneyCategoryEnum } from '@/types/money/category'
|
import { MoneyCategoryEnum } from '@/types/money/category'
|
||||||
|
|
||||||
interface CreateCategoryProps {
|
export interface EditCategoryForm {
|
||||||
|
id?: number
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditCategoryProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
type: MoneyCategoryEnum
|
type: MoneyCategoryEnum
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateCategoryForm {
|
onConfirm?: (form: EditCategoryForm) => Promise<void>
|
||||||
icon: string
|
|
||||||
name: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateCategory({
|
export default function CreateCategory({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
type,
|
type,
|
||||||
}: CreateCategoryProps) {
|
onConfirm,
|
||||||
|
}: EditCategoryProps) {
|
||||||
const moneyCategoryStore = useMoneyCategoryStore()
|
const moneyCategoryStore = useMoneyCategoryStore()
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(open)
|
const [isOpen, setIsOpen] = useState(open)
|
||||||
|
|
||||||
const [form, setForm] = useState<CreateCategoryForm>({
|
const [form, setForm] = useState<EditCategoryForm>({
|
||||||
icon: '',
|
icon: '',
|
||||||
name: '',
|
name: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const [confirmDisabled, setConfirmDisabled] = useState(true)
|
const [confirmDisabled, setConfirmDisabled] = useState(true)
|
||||||
const [confirmLoading, setConfirmLoading] = useState(false)
|
const [confirmLoading, setConfirmLoading] = useState(false)
|
||||||
|
|
||||||
@@ -46,13 +50,13 @@ export default function CreateCategory({
|
|||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (form.name && form.name.length < 6) {
|
if (form.name && form.name.length <= 5 && form.icon) {
|
||||||
setConfirmDisabled(false)
|
setConfirmDisabled(false)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
setConfirmDisabled(true)
|
setConfirmDisabled(true)
|
||||||
}
|
}
|
||||||
}, [form.name])
|
}, [form.name, form.icon])
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
@@ -79,15 +83,49 @@ export default function CreateCategory({
|
|||||||
|
|
||||||
<Text className="text-lg font-medium">{type === MoneyCategoryEnum.Expense ? '添加支出分类' : '添加收入分类'}</Text>
|
<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>
|
</View>
|
||||||
|
|
||||||
<Input
|
<AInput
|
||||||
variant="outline"
|
placeholder="请输入名称(不超过5个字符)"
|
||||||
>
|
onChangeText={(text) => {
|
||||||
<InputField placeholder="请输入名称(不超过5个字符)" />
|
setForm((val) => {
|
||||||
</Input>
|
return {
|
||||||
|
...val,
|
||||||
|
name: text,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<DrawerBody className="mb-16">
|
<DrawerBody className="mb-16">
|
||||||
{Object.entries(moneyCategoryStore.ICON_GROUPS).map(([groupName, icons]) => (
|
{Object.entries(moneyCategoryStore.ICON_GROUPS).map(([groupName, icons]) => (
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { SortableGridRenderItem } from 'react-native-sortables'
|
import type { SortableGridRenderItem } from 'react-native-sortables'
|
||||||
|
import type { EditCategoryForm } from './components/EditCategory'
|
||||||
import type { MoneyCategoryItem } from '@/types/money/category'
|
import type { MoneyCategoryItem } from '@/types/money/category'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
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 Animated, { useAnimatedRef } from 'react-native-reanimated'
|
||||||
import Sortable from 'react-native-sortables'
|
import Sortable from 'react-native-sortables'
|
||||||
import { AButton, ATab, LucideIcon } from '@/components/common/ao'
|
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 CustomHeader from '@/components/custom/Header'
|
||||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||||
import { MoneyCategoryEnum } from '@/types/money/category'
|
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() {
|
export default function MoneyCategorySetting() {
|
||||||
const [selectedTab, setSelectedTab] = useState(MoneyCategoryEnum.Expense)
|
const [selectedTab, setSelectedTab] = useState(MoneyCategoryEnum.Expense)
|
||||||
@@ -27,7 +29,7 @@ export default function MoneyCategorySetting() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCategories(selectedTab === MoneyCategoryEnum.Expense ? moneyCategoryStore.getExpenseCategories() : moneyCategoryStore.getIncomeCategories())
|
setCategories(selectedTab === MoneyCategoryEnum.Expense ? moneyCategoryStore.getExpenseCategories() : moneyCategoryStore.getIncomeCategories())
|
||||||
}, [selectedTab])
|
}, [selectedTab, moneyCategoryStore.categories])
|
||||||
|
|
||||||
const renderItem = useCallback<SortableGridRenderItem<MoneyCategoryItem>>(
|
const renderItem = useCallback<SortableGridRenderItem<MoneyCategoryItem>>(
|
||||||
({ item }) => (
|
({ 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 (
|
return (
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<CustomHeader
|
<CustomHeader
|
||||||
@@ -146,7 +184,6 @@ export default function MoneyCategorySetting() {
|
|||||||
// 设置 1 秒后删除
|
// 设置 1 秒后删除
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
const deleteId = Number(draggingId)
|
const deleteId = Number(draggingId)
|
||||||
setCategories(prev => prev.filter(c => c.id !== deleteId))
|
|
||||||
|
|
||||||
moneyCategoryStore.deleteCategoryById(deleteId)
|
moneyCategoryStore.deleteCategoryById(deleteId)
|
||||||
|
|
||||||
@@ -189,10 +226,11 @@ export default function MoneyCategorySetting() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<CreateCategory
|
<EditCategory
|
||||||
type={selectedTab}
|
type={selectedTab}
|
||||||
open={openCreateCategory}
|
open={openCreateCategory}
|
||||||
onClose={() => setOpenCreateCategory(false)}
|
onClose={() => setOpenCreateCategory(false)}
|
||||||
|
onConfirm={handleConfirmEdit}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -83,6 +83,21 @@ export default function AmountKeyboard({
|
|||||||
})
|
})
|
||||||
}, [record_time, amount, remark])
|
}, [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 handleChange = (newData: Partial<typeof internalData>) => {
|
||||||
const updated = { ...internalData, ...newData }
|
const updated = { ...internalData, ...newData }
|
||||||
setInternalData(updated)
|
setInternalData(updated)
|
||||||
@@ -98,9 +113,11 @@ export default function AmountKeyboard({
|
|||||||
newInput = prev.slice(0, -1)
|
newInput = prev.slice(0, -1)
|
||||||
}
|
}
|
||||||
else if (key === 'clear') {
|
else if (key === 'clear') {
|
||||||
const currentDate = getCurrentDate()
|
setInternalData({
|
||||||
handleChange({ record_time: currentDate, amount: '', remark: '' })
|
record_time: getCurrentDate(),
|
||||||
|
amount: '0',
|
||||||
|
remark: '',
|
||||||
|
})
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
else if (key === '/') {
|
else if (key === '/') {
|
||||||
@@ -190,15 +207,6 @@ export default function AmountKeyboard({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算结果
|
|
||||||
try {
|
|
||||||
const computed = calculateWithBig(newInput)
|
|
||||||
handleChange({ amount: computed })
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
handleChange({ amount: '0' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return newInput
|
return newInput
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function CategoryGrid({
|
|||||||
>
|
>
|
||||||
<MoneyCategoryIcon
|
<MoneyCategoryIcon
|
||||||
name={item.icon}
|
name={item.icon}
|
||||||
size={24}
|
size={28}
|
||||||
color={
|
color={
|
||||||
isSelected ? '#fff' : '#000'
|
isSelected ? '#fff' : '#000'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export default function MoenyRecordEditor() {
|
|||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
const result: MoneyRecordItem = {
|
const result: MoneyRecordItem = {
|
||||||
...res.data,
|
...res.data,
|
||||||
|
category_id: selectedCategory.id,
|
||||||
category_type: selectedCategory.type,
|
category_type: selectedCategory.type,
|
||||||
category_name: selectedCategory.name,
|
category_name: selectedCategory.name,
|
||||||
category_icon: selectedCategory.icon,
|
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'
|
import alovaInstance from '../../index'
|
||||||
|
|
||||||
const API_PREFIX = '/money/category'
|
const API_PREFIX = '/money/category'
|
||||||
@@ -14,3 +14,7 @@ export function deleteMoneyCategoryApi(params: DeleteMoneyCategoryParams) {
|
|||||||
export function updateMoneyCategorySortApi(params: UpdateMoneyCategorySortParams) {
|
export function updateMoneyCategorySortApi(params: UpdateMoneyCategorySortParams) {
|
||||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/updateSort`, params)
|
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({
|
const alovaInstance = createAlova({
|
||||||
baseURL: 'http://192.168.5.6:8086/app',
|
// baseURL: 'http://192.168.5.6:8086/app',
|
||||||
// baseURL: 'http://192.168.1.137:8086/app',
|
baseURL: 'http://192.168.1.137:8086/app',
|
||||||
requestAdapter: axiosRequestAdapter(),
|
requestAdapter: axiosRequestAdapter(),
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
cacheFor: null,
|
cacheFor: null,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { MoneyCategoryItem, UpdateMoneyCategorySortItem } from '@/types/money/category'
|
import type { MoneyCategoryItem, UpdateMoneyCategorySortItem } from '@/types/money/category'
|
||||||
import { create } from 'zustand'
|
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 {
|
interface MoneyCategoryStore {
|
||||||
ICON_GROUPS: Record<string, string[]>
|
ICON_GROUPS: Record<string, string[]>
|
||||||
@@ -8,15 +9,17 @@ interface MoneyCategoryStore {
|
|||||||
|
|
||||||
init: () => Promise<void>
|
init: () => Promise<void>
|
||||||
setCategories: (categories: MoneyCategoryItem[]) => void
|
setCategories: (categories: MoneyCategoryItem[]) => void
|
||||||
addCategory: (category: MoneyCategoryItem) => void
|
|
||||||
|
|
||||||
getExpenseCategories: () => MoneyCategoryItem[]
|
getExpenseCategories: () => MoneyCategoryItem[]
|
||||||
getIncomeCategories: () => MoneyCategoryItem[]
|
getIncomeCategories: () => MoneyCategoryItem[]
|
||||||
|
|
||||||
getCategoryById: (id: number) => MoneyCategoryItem | null
|
getCategoryById: (id: number) => MoneyCategoryItem | null
|
||||||
|
|
||||||
updateSortNumbers: (data: MoneyCategoryItem[], type: number) => void
|
createCategory: (category: MoneyCategoryItem) => Promise<void>
|
||||||
deleteCategoryById: (id: number) => void
|
updateSortNumbers: (data: MoneyCategoryItem[], type: number) => Promise<void>
|
||||||
|
deleteCategoryById: (id: number) => Promise<void>
|
||||||
|
|
||||||
|
isDuplicateCategory: (name: string, type: number) => boolean
|
||||||
|
|
||||||
clear: () => void
|
clear: () => void
|
||||||
}
|
}
|
||||||
@@ -94,9 +97,6 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
|||||||
|
|
||||||
setCategories: categories => set({ categories }),
|
setCategories: categories => set({ categories }),
|
||||||
|
|
||||||
addCategory: category =>
|
|
||||||
set({ categories: [...get().categories, category] }),
|
|
||||||
|
|
||||||
init: async () => {
|
init: async () => {
|
||||||
try {
|
try {
|
||||||
await queryMoneyCategoryApi().then((res) => {
|
await queryMoneyCategoryApi().then((res) => {
|
||||||
@@ -118,63 +118,98 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
|||||||
set({ categories: [] })
|
set({ categories: [] })
|
||||||
},
|
},
|
||||||
|
|
||||||
getExpenseCategories: () => (get().categories || []).filter(c => c.type === 0),
|
getExpenseCategories: () =>
|
||||||
getIncomeCategories: () => (get().categories || []).filter(c => c.type === 1),
|
(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) => {
|
getCategoryById: (id: number) => {
|
||||||
return get().categories.find(c => c.id === id) || null
|
return get().categories.find(c => c.id === id) || null
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSortNumbers: (data, type) => {
|
createCategory: async (category) => {
|
||||||
// 获取当前所有分类
|
const categories = category.type === MoneyCategoryEnum.Expense ? get().getExpenseCategories() : get().getIncomeCategories()
|
||||||
const allCategories = get().categories
|
|
||||||
|
|
||||||
// 保留另一类分类
|
const maxSortNumber = categories[categories.length - 1].sort_number + 1 || 1
|
||||||
const otherCategories = allCategories.filter(c => c.type !== type)
|
|
||||||
|
|
||||||
// 给当前类型的数据重新计算 sort_number
|
try {
|
||||||
const updatedData = data.map((item, index) => ({
|
const res = await createMoneyCategoryApi({
|
||||||
...item,
|
name: category.name,
|
||||||
sort_number: index, // 按顺序重新赋值
|
icon: category.icon,
|
||||||
}))
|
type: category.type,
|
||||||
|
sort_number: maxSortNumber,
|
||||||
// 合并另一类分类
|
|
||||||
const finalCategories = [...otherCategories, ...updatedData]
|
|
||||||
|
|
||||||
// 构造后端请求 payload,只需要 id 和 sort_number
|
|
||||||
const payload: UpdateMoneyCategorySortItem[] = updatedData.map(item => ({
|
|
||||||
id: item.id,
|
|
||||||
sort_number: item.sort_number,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 调用后端接口更新排序
|
|
||||||
updateMoneyCategorySortApi({
|
|
||||||
data: payload,
|
|
||||||
}).then((res) => {
|
|
||||||
console.log('后端返回:', res)
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!res.isSuccess) {
|
||||||
|
throw new Error(res.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
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: index,
|
||||||
|
}))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await updateMoneyCategorySortApi({ data: payload })
|
||||||
|
if (!res.isSuccess) {
|
||||||
|
throw new Error(res.message)
|
||||||
|
}
|
||||||
|
|
||||||
// 更新前端 state
|
// 更新前端 state
|
||||||
set({ categories: finalCategories })
|
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) => {
|
deleteCategoryById: async (id: number) => {
|
||||||
const allCategories = get().categories
|
try {
|
||||||
|
const res = await deleteMoneyCategoryApi({ id })
|
||||||
// 删除指定 id,同时只处理同类型的数据
|
if (!res.isSuccess) {
|
||||||
const finalCategories = allCategories
|
throw new Error(res.message)
|
||||||
.filter(c => c.id !== id)
|
}
|
||||||
|
|
||||||
// 更新 state
|
// 更新 state
|
||||||
|
const allCategories = get().categories
|
||||||
|
const finalCategories = allCategories.filter(c => c.id !== id)
|
||||||
set({ categories: finalCategories })
|
set({ categories: finalCategories })
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 调用删除接口
|
isDuplicateCategory: (name, type) => {
|
||||||
await deleteMoneyCategoryApi({ id }).catch((err) => {
|
const categories = get().categories || []
|
||||||
console.log(err)
|
return categories.some(c => c.name === name && c.type === type)
|
||||||
})
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
import type { MoneyRecordGroup, MoneyRecordItem } from '@/types/money/record'
|
import type { MoneyRecordGroup, MoneyRecordItem } from '@/types/money/record'
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { deleteMoneyRecordApi } from '@/request/api/money/record'
|
import { deleteMoneyRecordApi } from '@/request/api/money/record'
|
||||||
|
import { MoneyCategoryEnum } from '@/types/money/category'
|
||||||
import { add, sub } from '@/utils/math'
|
import { add, sub } from '@/utils/math'
|
||||||
import { isInMonth } from '@/utils/time'
|
import { isInMonth } from '@/utils/time'
|
||||||
import { useMoneyCategoryStore } from './moneyCategory'
|
|
||||||
|
|
||||||
interface MoneyRecordStore {
|
interface MoneyRecordStore {
|
||||||
records: MoneyRecordGroup[]
|
records: MoneyRecordGroup[]
|
||||||
currentMonth: string
|
currentMonth: string
|
||||||
|
|
||||||
total_income: string
|
/* computed */
|
||||||
total_expense: string
|
getTotalIncome: () => string
|
||||||
total_balance: 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
|
setCurrentMonth: (month: string) => void
|
||||||
|
|
||||||
addMoneyRecord: (data: MoneyRecordItem) => void
|
addMoneyRecord: (data: MoneyRecordItem) => void
|
||||||
@@ -23,164 +24,77 @@ interface MoneyRecordStore {
|
|||||||
getMoneyRecordById: (id: number) => MoneyRecordItem | null
|
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) => ({
|
export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||||
records: [],
|
records: [],
|
||||||
currentMonth: '',
|
currentMonth: '',
|
||||||
|
|
||||||
total_income: '0.00',
|
/* ---------------- computed ---------------- */
|
||||||
total_expense: '0.00',
|
getTotalIncome: () => {
|
||||||
total_balance: '0.00',
|
return get().records.reduce((sum, g) => add(sum, g.income), '0.00')
|
||||||
|
},
|
||||||
|
|
||||||
setRecords: (records, total_expense, total_income, total_balance, month) => {
|
getTotalExpense: () => {
|
||||||
set({ records, currentMonth: month, total_income, total_expense, total_balance })
|
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 }),
|
setCurrentMonth: month => set({ currentMonth: month }),
|
||||||
|
|
||||||
|
/* ---------------- actions ---------------- */
|
||||||
addMoneyRecord: (data) => {
|
addMoneyRecord: (data) => {
|
||||||
set((prev) => {
|
if (!isInMonth(data.record_time, get().currentMonth))
|
||||||
if (!isInMonth(data.record_time, prev.currentMonth))
|
return
|
||||||
return prev
|
const flatRecords = [...get().records.flatMap(g => g.records), data]
|
||||||
|
const newGroups = recomputeGroups(flatRecords)
|
||||||
const category = useMoneyCategoryStore.getState().getById(data.category_id)
|
set({ records: newGroups })
|
||||||
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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateMoneyRecord: (updated) => {
|
updateMoneyRecord: (updated) => {
|
||||||
set((prev) => {
|
const flatRecords = get().records.flatMap(g =>
|
||||||
const records = [...prev.records]
|
g.records.map(r => r.id === updated.id ? updated : r),
|
||||||
const oldGroupIndex = records.findIndex(g => g.records.some(r => r.id === updated.id))
|
)
|
||||||
if (oldGroupIndex === -1)
|
// 如果更新后的记录不在当前月,则直接删除
|
||||||
return prev
|
const filteredRecords = flatRecords.filter(r => isInMonth(r.record_time, get().currentMonth))
|
||||||
|
const newGroups = recomputeGroups(filteredRecords)
|
||||||
const updateRecordCategory = useMoneyCategoryStore.getState().getById((updated.category_id))
|
set({ records: newGroups })
|
||||||
|
|
||||||
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 }
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteMoneyRecord: (id) => {
|
deleteMoneyRecord: (id) => {
|
||||||
set((prev) => {
|
const flatRecords = get().records.flatMap(g => g.records).filter(r => r.id !== id)
|
||||||
const records = prev.records.map(g => ({ ...g, records: [...g.records] }))
|
const newGroups = recomputeGroups(flatRecords)
|
||||||
let total_expense = prev.total_expense
|
set({ records: newGroups })
|
||||||
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 }
|
|
||||||
})
|
|
||||||
|
|
||||||
deleteMoneyRecordApi({ id }).catch(console.error)
|
deleteMoneyRecordApi({ id }).catch(console.error)
|
||||||
},
|
},
|
||||||
|
|
||||||
getMoneyRecordById: (id: number) => {
|
getMoneyRecordById: (id) => {
|
||||||
const records = get().records
|
for (const group of get().records) {
|
||||||
for (const group of records) {
|
|
||||||
const record = group.records.find(r => r.id === id)
|
const record = group.records.find(r => r.id === id)
|
||||||
if (record)
|
if (record)
|
||||||
return record
|
return record
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface CreateMoneyCategoryParams {
|
|||||||
name: string
|
name: string
|
||||||
icon: string
|
icon: string
|
||||||
type: MoneyCategoryEnum
|
type: MoneyCategoryEnum
|
||||||
|
sort_number: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateMoneyCategoryResult {
|
export interface CreateMoneyCategoryResult {
|
||||||
|
|||||||
Reference in new issue
Block a user