This commit is contained in:
oneao committed 2026-01-16 17:27:34 +08:00
1 parent 44ab5416a3
commit 1935906676
17 files changed
+321 -265

No files matched your search

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