diff --git a/code/allapp/allapp-go/biz/handler/money/money_category_service.go b/code/allapp/allapp-go/biz/handler/money/money_category_service.go index 76110edc..609ffdbf 100644 --- a/code/allapp/allapp-go/biz/handler/money/money_category_service.go +++ b/code/allapp/allapp-go/biz/handler/money/money_category_service.go @@ -10,6 +10,7 @@ import ( "allapp/utils/idgen" "allapp/utils/response" "context" + "fmt" 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) { var err error var req money.CreateMoneyCategoryReq + err = c.BindAndValidate(&req) if err != nil { + fmt.Println(err) + c.String(consts.StatusBadRequest, err.Error()) return } @@ -134,13 +138,12 @@ func CreateMoneyCategory(ctx context.Context, c *app.RequestContext) { ID := idgen.NextId() params := repo.InsertMoneyUserCategoryParams{ - ID: ID, - UserID: middleware.GetUserID(ctx), - Name: req.Name, - Icon: req.Icon, - Type: req.Type, - // 默认排序号为ID - SortNumber: 0, + ID: ID, + UserID: middleware.GetUserID(ctx), + Name: req.Name, + Icon: req.Icon, + Type: req.Type, + SortNumber: req.SortNumber, } // 插入 diff --git a/code/allapp/allapp-go/biz/model/money/money_category_types.pb.go b/code/allapp/allapp-go/biz/model/money/money_category_types.pb.go index dfa944d3..d7a7f90b 100644 --- a/code/allapp/allapp-go/biz/model/money/money_category_types.pb.go +++ b/code/allapp/allapp-go/biz/model/money/money_category_types.pb.go @@ -423,9 +423,10 @@ type CreateMoneyCategoryReq struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=Name,proto3" form:"name" json:"name" vd:"$ != '' && len($) <= 5"` - 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"` + 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:"$ != ''"` + 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() { @@ -481,6 +482,13 @@ func (x *CreateMoneyCategoryReq) GetType() int32 { return 0 } +func (x *CreateMoneyCategoryReq) GetSortNumber() int32 { + if x != nil { + return x.SortNumber + } + return 0 +} + type CreateMoneyCategoryResp struct { state protoimpl.MessageState 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, 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, - 0x73, 0x70, 0x22, 0x97, 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, - 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x22, 0xca, 0xbb, 0x18, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0xda, 0xbb, 0x18, 0x16, 0x24, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, - 0x20, 0x26, 0x26, 0x20, 0x6c, 0x65, 0x6e, 0x28, 0x24, 0x29, 0x20, 0x3c, 0x3d, 0x20, 0x35, 0x52, - 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x04, 0x49, 0x63, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x13, 0xca, 0xbb, 0x18, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0xda, 0xbb, 0x18, - 0x07, 0x24, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x52, 0x04, 0x49, 0x63, 0x6f, 0x6e, 0x12, 0x1c, - 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x42, 0x08, 0xca, 0xbb, - 0x18, 0x04, 0x74, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x22, 0x31, 0x0a, 0x17, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x6f, 0x6e, 0x65, 0x79, 0x43, 0x61, 0x74, 0x65, 0x67, - 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x12, 0x16, 0x0a, 0x02, 0x49, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x03, 0x42, 0x06, 0xca, 0xbb, 0x18, 0x02, 0x69, 0x64, 0x52, 0x02, 0x49, 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, + 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, 0x27, 0x0a, + 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x13, 0xca, 0xbb, 0x18, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0xda, 0xbb, 0x18, 0x07, 0x24, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, + 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x04, 0x49, 0x63, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x13, 0xca, 0xbb, 0x18, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0xda, 0xbb, + 0x18, 0x07, 0x24, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x52, 0x04, 0x49, 0x63, 0x6f, 0x6e, 0x12, + 0x1c, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x42, 0x08, 0xca, + 0xbb, 0x18, 0x04, 0x74, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2f, 0x0a, + 0x0a, 0x53, 0x6f, 0x72, 0x74, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x05, 0x42, 0x0f, 0xca, 0xbb, 0x18, 0x0b, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6e, 0x75, 0x6d, 0x62, + 0x65, 0x72, 0x52, 0x0a, 0x53, 0x6f, 0x72, 0x74, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x31, + 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x6f, 0x6e, 0x65, 0x79, 0x43, 0x61, 0x74, + 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x12, 0x16, 0x0a, 0x02, 0x49, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x06, 0xca, 0xbb, 0x18, 0x02, 0x69, 0x64, 0x52, 0x02, 0x49, + 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 ( diff --git a/code/allapp/allapp-go/db/queries/money_record.sql b/code/allapp/allapp-go/db/queries/money_record.sql index 15b92082..8e0bf310 100644 --- a/code/allapp/allapp-go/db/queries/money_record.sql +++ b/code/allapp/allapp-go/db/queries/money_record.sql @@ -31,9 +31,11 @@ INSERT INTO money_record ( user_id, amount, remark, - record_time + record_time, + create_time, + update_time ) VALUES ( - $1, $2, $3, $4, $5,$6 + $1, $2, $3, $4, $5,$6,NOW(),NOW() ) RETURNING *; diff --git a/code/allapp/allapp-go/db/repo/money_record.sql.go b/code/allapp/allapp-go/db/repo/money_record.sql.go index 190976a6..c52f4821 100644 --- a/code/allapp/allapp-go/db/repo/money_record.sql.go +++ b/code/allapp/allapp-go/db/repo/money_record.sql.go @@ -181,9 +181,11 @@ INSERT INTO money_record ( user_id, amount, remark, - record_time + record_time, + create_time, + update_time ) 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 ` diff --git a/code/allapp/allapp-go/idl/money/money_category_types.proto b/code/allapp/allapp-go/idl/money/money_category_types.proto index 16e6abb8..c01ada2b 100644 --- a/code/allapp/allapp-go/idl/money/money_category_types.proto +++ b/code/allapp/allapp-go/idl/money/money_category_types.proto @@ -47,13 +47,14 @@ message UpdateMoneyCategorySortResp {} message CreateMoneyCategoryReq { string Name = 1 [ (api.body)="name", - (api.vd) = "$ != '' && len($) <= 5" + (api.vd) = "$ != ''" ]; string Icon = 2 [ (api.body)="icon", (api.vd) = "$ != ''" ]; int32 Type = 3 [(api.body)="type"]; + int32 SortNumber = 4 [(api.body)="sort_number"]; } message CreateMoneyCategoryResp { diff --git a/code/allapp/allapp-rn/src/app/(tabs)/money/components/TransactionsList.tsx b/code/allapp/allapp-rn/src/app/(tabs)/money/components/TransactionsList.tsx index 173ce458..6baed1fe 100644 --- a/code/allapp/allapp-rn/src/app/(tabs)/money/components/TransactionsList.tsx +++ b/code/allapp/allapp-rn/src/app/(tabs)/money/components/TransactionsList.tsx @@ -146,7 +146,7 @@ export default function StaticTransactions({ }} > - + diff --git a/code/allapp/allapp-rn/src/app/(tabs)/money/index.tsx b/code/allapp/allapp-rn/src/app/(tabs)/money/index.tsx index 9965583a..00ebcf83 100644 --- a/code/allapp/allapp-rn/src/app/(tabs)/money/index.tsx +++ b/code/allapp/allapp-rn/src/app/(tabs)/money/index.tsx @@ -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()} @@ -154,7 +153,7 @@ export default function Money() { numberOfLines={1} ellipsizeMode="tail" > - {moneyRecordStore.total_expense} + {moneyRecordStore.getTotalExpense()} @@ -165,7 +164,7 @@ export default function Money() { numberOfLines={1} ellipsizeMode="tail" > - {moneyRecordStore.total_balance} + {moneyRecordStore.getTotalBalance()} diff --git a/code/allapp/allapp-rn/src/app/money/category/setting/components/CreateCategory.tsx b/code/allapp/allapp-rn/src/app/money/category/setting/components/EditCategory.tsx similarity index 69% rename from code/allapp/allapp-rn/src/app/money/category/setting/components/CreateCategory.tsx rename to code/allapp/allapp-rn/src/app/money/category/setting/components/EditCategory.tsx index 07bffe15..68a8c2a6 100644 --- a/code/allapp/allapp-rn/src/app/money/category/setting/components/CreateCategory.tsx +++ b/code/allapp/allapp-rn/src/app/money/category/setting/components/EditCategory.tsx @@ -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 } export default function CreateCategory({ open, onClose, type, -}: CreateCategoryProps) { + onConfirm, +}: EditCategoryProps) { const moneyCategoryStore = useMoneyCategoryStore() const [isOpen, setIsOpen] = useState(open) - const [form, setForm] = useState({ + const [form, setForm] = useState({ 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({ {type === MoneyCategoryEnum.Expense ? '添加支出分类' : '添加收入分类'} - 确认 + { + 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) + } + }} + > + 确认 + - - - - + { + setForm((val) => { + return { + ...val, + name: text, + } + }) + }} + /> {Object.entries(moneyCategoryStore.ICON_GROUPS).map(([groupName, icons]) => ( diff --git a/code/allapp/allapp-rn/src/app/money/category/setting/index.tsx b/code/allapp/allapp-rn/src/app/money/category/setting/index.tsx index e880851d..cdbc1b24 100644 --- a/code/allapp/allapp-rn/src/app/money/category/setting/index.tsx +++ b/code/allapp/allapp-rn/src/app/money/category/setting/index.tsx @@ -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>( ({ 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 ( { const deleteId = Number(draggingId) - setCategories(prev => prev.filter(c => c.id !== deleteId)) moneyCategoryStore.deleteCategoryById(deleteId) @@ -189,10 +226,11 @@ export default function MoneyCategorySetting() { - setOpenCreateCategory(false)} + onConfirm={handleConfirmEdit} /> ) diff --git a/code/allapp/allapp-rn/src/app/money/record/editor/components/AmountKeyboard.tsx b/code/allapp/allapp-rn/src/app/money/record/editor/components/AmountKeyboard.tsx index 7773994c..20e03858 100644 --- a/code/allapp/allapp-rn/src/app/money/record/editor/components/AmountKeyboard.tsx +++ b/code/allapp/allapp-rn/src/app/money/record/editor/components/AmountKeyboard.tsx @@ -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) => { 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 }) } diff --git a/code/allapp/allapp-rn/src/app/money/record/editor/components/CategoryGrid.tsx b/code/allapp/allapp-rn/src/app/money/record/editor/components/CategoryGrid.tsx index b5559a23..f2301c61 100644 --- a/code/allapp/allapp-rn/src/app/money/record/editor/components/CategoryGrid.tsx +++ b/code/allapp/allapp-rn/src/app/money/record/editor/components/CategoryGrid.tsx @@ -66,7 +66,7 @@ export default function CategoryGrid({ > >(`${API_PREFIX}/updateSort`, params) } + +export function createMoneyCategoryApi(params: CreateMoneyCategoryParams) { + return alovaInstance.Post>(`${API_PREFIX}/create`, params) +} diff --git a/code/allapp/allapp-rn/src/request/index.ts b/code/allapp/allapp-rn/src/request/index.ts index de7e4c95..476eb11d 100644 --- a/code/allapp/allapp-rn/src/request/index.ts +++ b/code/allapp/allapp-rn/src/request/index.ts @@ -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, diff --git a/code/allapp/allapp-rn/src/store/moneyCategory.ts b/code/allapp/allapp-rn/src/store/moneyCategory.ts index 3d27bf67..d0b48ef2 100644 --- a/code/allapp/allapp-rn/src/store/moneyCategory.ts +++ b/code/allapp/allapp-rn/src/store/moneyCategory.ts @@ -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 @@ -8,15 +9,17 @@ interface MoneyCategoryStore { init: () => Promise 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 + updateSortNumbers: (data: MoneyCategoryItem[], type: number) => Promise + deleteCategoryById: (id: number) => Promise + + isDuplicateCategory: (name: string, type: number) => boolean clear: () => void } @@ -94,9 +97,6 @@ export const useMoneyCategoryStore = create()( 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()( 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) }, }), - ) diff --git a/code/allapp/allapp-rn/src/store/moneyRecord.ts b/code/allapp/allapp-rn/src/store/moneyRecord.ts index 4f07e3b6..c0edfa1a 100644 --- a/code/allapp/allapp-rn/src/store/moneyRecord.ts +++ b/code/allapp/allapp-rn/src/store/moneyRecord.ts @@ -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 = {} + + 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((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 diff --git a/code/allapp/allapp-rn/src/types/money/category.ts b/code/allapp/allapp-rn/src/types/money/category.ts index dfb80b65..3944158d 100644 --- a/code/allapp/allapp-rn/src/types/money/category.ts +++ b/code/allapp/allapp-rn/src/types/money/category.ts @@ -38,6 +38,7 @@ export interface CreateMoneyCategoryParams { name: string icon: string type: MoneyCategoryEnum + sort_number: number } export interface CreateMoneyCategoryResult {