This commit is contained in:
oneao committed 2026-03-04 17:30:05 +08:00
1 parent 46106d0c76
commit 34240d2e16
36 files changed
+2853 -165

No files matched your search

+373
View File
@@ -0,0 +1,373 @@
import type { FinanceCategory, IconGroupType } from '@/types/finance/category'
import type { FinanceRecord, FinanceRecordGroup } from '@/types/finance/record'
import { create } from 'zustand'
import { listFinanceCategoriesApi } from '@/request/api/finance/category'
import { FinanceCategoryEnum } from '@/types/finance/category'
import { add, sub } from '@/utils/math'
interface FinanceState {
// 分类
DefaultGroupCategories: IconGroupType[]
categories: FinanceCategory[]
setCategories: (categories: FinanceCategory[]) => void
ensureCategoriesLoaded: () => Promise<void>
getExpenseCategories: () => FinanceCategory[]
getIncomeCategories: () => FinanceCategory[]
createCategory: (category: FinanceCategory) => void
updateCategory: (category: FinanceCategory) => void
deleteCategory: (id: number) => void
updateCategorySortNumber: (data: FinanceCategory[], type: number) => void
isDuplicateCategory: (name: string, type: number, excludeId: number | null) => boolean
getDefaultGroupRow: (iconName: string) => number
getCategoryById: (id: number) => FinanceCategory | null
getCategoryNextSortNumber: (type: number) => number
// 记录
records: FinanceRecordGroup[]
getTotalIncome: () => string
getTotalExpense: () => string
getTotalBalance: () => string
setRecords: (records: FinanceRecord[]) => void
getRecordById: (id: number) => FinanceRecord | null
}
function recomputeGroups(records: FinanceRecord[]) {
const { getCategoryById } = useFinanceStore.getState()
const groupsMap: Record<string, FinanceRecordGroup> = {}
records.forEach((r) => {
const category = getCategoryById(r.category_id)
if (!category)
return
const group = groupsMap[r.record_time] ?? {
record_time: r.record_time,
records: [],
income: '0.00',
expense: '0.00',
}
group.records.push(r)
if (category.type === FinanceCategoryEnum.Expense)
group.expense = add(group.expense, r.amount)
if (category.type === FinanceCategoryEnum.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 useFinanceStore = create<FinanceState>()(
(set, get) => ({
// ================= 分类 =================
DefaultGroupCategories: [
{
index: 0,
type: 'title',
value: '餐饮',
},
{
index: 1,
type: 'icons',
value: [
'money_canyin',
'money_yinpin',
'money_lingshi',
'money_xiaoye',
],
},
{
index: 2,
type: 'title',
value: '娱乐',
},
{
index: 3,
type: 'icons',
value: [
'money_yule',
'money_youxi',
'money_dianying',
],
},
{
index: 4,
type: 'title',
value: '交通',
},
{
index: 5,
type: 'icons',
value: [
'money_jiaotong',
'money_tingchefei',
'money_jiayou',
],
},
{
index: 6,
type: 'title',
value: '生活消费',
},
{
index: 7,
type: 'icons',
value: [
'money_lijin',
'money_jiadian',
'money_riyongpin',
'money_shucai',
'money_meifa',
],
},
{
index: 8,
type: 'icons',
value: [
'money_shuidian',
'money_wenju',
'money_zhufang',
'money_baoxian',
'money_baojianpin',
],
},
{
index: 9,
type: 'icons',
value: [
'money_huafei',
'money_lvyou',
'money_gouwu',
'money_shuben',
'money_chongwu',
],
},
{
index: 10,
type: 'icons',
value: [
'money_ruanjian',
],
},
{
index: 11,
type: 'title',
value: '健康',
},
{
index: 12,
type: 'icons',
value: [
'money_tijian',
'money_yao',
'money_jiuyi',
],
},
{
index: 13,
type: 'title',
value: '公益',
},
{
index: 14,
type: 'icons',
value: [
'money_gongyi',
],
},
{
index: 15,
type: 'title',
value: '兼职与收入',
},
{
index: 16,
type: 'icons',
value: [
'money_gongzi',
'money_jianzhi',
],
},
{
index: 17,
type: 'title',
value: '鲜花与水果',
},
{
index: 18,
type: 'icons',
value: [
'money_shuiguo',
'money_xianhua',
],
},
{
index: 19,
type: 'title',
value: '转账与转移',
},
{
index: 20,
type: 'icons',
value: [
'money_zhuanzhang',
],
},
{
index: 21,
type: 'title',
value: '家庭与家电',
},
{
index: 22,
type: 'icons',
value: [
'money_kuaidi',
'money_weixiu',
'money_jiaju',
'money_muying',
'money_liwu',
],
},
{
index: 23,
type: 'title',
value: '运动与娱乐',
},
{
index: 24,
type: 'icons',
value: [
'money_yundong',
'money_peixun',
],
},
],
categories: [],
setCategories: categories =>
set({
categories: [...categories].sort(
(a, b) => (a.sort_number ?? 0) - (b.sort_number ?? 0),
),
}),
ensureCategoriesLoaded: async () => {
if (get().categories.length > 0)
return
const res = await listFinanceCategoriesApi()
if (res.isSuccess) {
set({ categories: res.data.categories })
}
},
getExpenseCategories: () =>
get().categories.filter(c => c.type === FinanceCategoryEnum.Expense),
getIncomeCategories: () =>
get().categories.filter(c => c.type === FinanceCategoryEnum.Income),
getCategoryById: (id: number) => {
return get().categories.find(c => c.id === id) || null
},
createCategory: (category) => {
set({
categories: [...get().categories, category],
})
},
updateCategory: (category) => {
set(state => ({
categories: state.categories.map(c =>
c.id !== category.id
? c
: {
...c,
...category,
},
),
}))
},
deleteCategory: (id) => {
const allCategories = get().categories
const finalCategories = allCategories.filter(c => c.id !== id)
set({ categories: finalCategories })
},
updateCategorySortNumber: (data, type) => {
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] })
},
isDuplicateCategory: (name, type, excludeId) => {
const categories = get().categories || []
return categories.some(c =>
c.name === name
&& c.type === type
&& c.id !== excludeId,
)
},
getDefaultGroupRow: (iconName) => {
const iconGroups = get().DefaultGroupCategories
for (const group of iconGroups) {
if (group.type === 'icons' && group.value.includes(iconName)) {
return group.index
}
}
return 0
},
getCategoryNextSortNumber: (type: number): number => {
const categories = get().categories.filter(c => c.type === type)
if (categories.length === 0) {
return 0
}
const maxSortNumber = Math.max(...categories.map(c => c.sort_number))
return maxSortNumber + 1
},
// ================= 分类 =================
// ================= 记录 =================
records: [],
getTotalIncome: () => {
return get().records.reduce((sum, g) => add(sum, g.income), '0.00')
},
getTotalExpense: () => {
return get().records.reduce((sum, g) => add(sum, g.expense), '0.00')
},
getTotalBalance: () => {
return sub(get().getTotalIncome(), get().getTotalExpense())
},
setRecords: (records) => {
const newGroups = recomputeGroups(records)
set({ records: newGroups })
},
getRecordById: (id) => {
for (const group of get().records) {
const record = group.records.find(r => r.id === id)
if (record)
return record
}
return null
},
// ================= 记录 =================
}),
)
@@ -19,7 +19,7 @@ interface NavState {
}
const DEFAULT_NAV_MENUS: NavItem[] = [
{ label: '记账', path: '/money', icon: 'CircleDollarSign' },
{ label: '记账', path: '/finance', icon: 'CircleDollarSign' },
{ label: '恋爱记', path: '/love', icon: 'Heart' },
{ label: '聊天', path: '/chat', icon: 'MessageCircle' },
{ label: '个人中心', path: '/user', icon: 'CircleUserRound' },