This commit is contained in:
oneao committed 2026-01-16 17:27:34 +08:00
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>
@@ -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)
}
+2 -2
View File
@@ -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)
})
}, },
}), }),
) )
+57 -143
View File
@@ -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 {