u
This commit is contained in:
1 parent
46106d0c76
commit
34240d2e16
36 files changed
+2853
-165
No files matched your search
@@ -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' },
|
||||
|
||||
Reference in new issue
Block a user