u
This commit is contained in:
1 parent
6277fb2f9e
commit
8ecac0d7dd
270 files changed
+40438
-529
No files matched your search
@@ -0,0 +1,60 @@
|
||||
import { create } from 'zustand'
|
||||
import { getUserSpaceApi } from '@/request/api/space/spaceUser'
|
||||
import { useMoneyCategoryStore } from './moneyCategory'
|
||||
import { useNavStore } from './navigation'
|
||||
import { useSpaceStore } from './space'
|
||||
import { useUserStore } from './user'
|
||||
|
||||
interface AppState {
|
||||
ready: boolean
|
||||
setReady: (v: boolean) => void
|
||||
init: () => Promise<void>
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>((set, get) => ({
|
||||
ready: false,
|
||||
|
||||
setReady: (v: boolean) => set({ ready: v }),
|
||||
|
||||
init: async () => {
|
||||
// 如果已经初始化过就不重复执行(除非你手动 setReady(false) 再 init)
|
||||
if (get().ready)
|
||||
return
|
||||
|
||||
const userStore = useUserStore.getState()
|
||||
const navStore = useNavStore.getState()
|
||||
const moneyCategoryStore = useMoneyCategoryStore.getState()
|
||||
const spaceStore = useSpaceStore.getState()
|
||||
|
||||
// 未登录:跳转到登录页,但认为初始化已完成(允许 UI 继续渲染)
|
||||
if (!userStore.isLoggedIn()) {
|
||||
navStore.navigateToAuth()
|
||||
set({ ready: true })
|
||||
return
|
||||
}
|
||||
|
||||
// 登录状态:执行完整初始化流程
|
||||
try {
|
||||
// 分类初始化
|
||||
await moneyCategoryStore.init()
|
||||
|
||||
// 获取空间信息
|
||||
const res = await getUserSpaceApi({})
|
||||
spaceStore.setUserSpace(res.data)
|
||||
}
|
||||
catch (err) {
|
||||
console.error('app init error:', err)
|
||||
}
|
||||
|
||||
// 初始化完成
|
||||
set({ ready: true })
|
||||
},
|
||||
reset: () => {
|
||||
set({ ready: false })
|
||||
|
||||
useMoneyCategoryStore.getState().clear()
|
||||
useUserStore.getState().clear()
|
||||
useSpaceStore.getState().clear()
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,147 @@
|
||||
import type { MoneyCategoryItem, UpdateMoneyCategorySortItem } from '@/types/money/category'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { create } from 'zustand'
|
||||
import { createJSONStorage, persist } from 'zustand/middleware'
|
||||
import { deleteMoneyCategoryApi, queryMoneyCategoryApi, updateMoneyCategorySortApi } from '@/request/api/money/category'
|
||||
|
||||
interface MoneyCategoryStore {
|
||||
categories: MoneyCategoryItem[]
|
||||
|
||||
setCategories: (categories: MoneyCategoryItem[]) => void
|
||||
addCategory: (category: MoneyCategoryItem) => void
|
||||
removeCategory: (id: number) => void
|
||||
init: () => Promise<void>
|
||||
|
||||
getExpenseCategories: () => MoneyCategoryItem[]
|
||||
getIncomeCategories: () => MoneyCategoryItem[]
|
||||
|
||||
getCategoryById: (id: number) => MoneyCategoryItem | null
|
||||
|
||||
updateCategories: (data: MoneyCategoryItem[], type: number) => void
|
||||
deleteCategoryById: (id: number, type: number) => void
|
||||
|
||||
sysCategories: MoneyCategoryItem[]
|
||||
setSysCategories: (categories: MoneyCategoryItem[]) => void
|
||||
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
categories: [],
|
||||
sysCategories: [],
|
||||
|
||||
setCategories: categories => set({ categories }),
|
||||
setSysCategories: categories => set({ categories }),
|
||||
|
||||
addCategory: category =>
|
||||
set({ categories: [...get().categories, category] }),
|
||||
|
||||
removeCategory: id =>
|
||||
set({ categories: get().categories.filter(c => c.id !== id) }),
|
||||
|
||||
init: async () => {
|
||||
try {
|
||||
await queryMoneyCategoryApi().then((res) => {
|
||||
if (res.isSuccess) {
|
||||
set({ categories: res.data.categories || [] })
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
}).finally(() => {
|
||||
|
||||
})
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Fetch category failed:', e)
|
||||
}
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
set({ categories: [] })
|
||||
},
|
||||
|
||||
getExpenseCategories: () => (get().categories || []).filter(c => c.type === 0),
|
||||
getIncomeCategories: () => (get().categories || []).filter(c => c.type === 1),
|
||||
|
||||
getCategoryById: (id: number) => {
|
||||
return get().categories.find(c => c.id === id) || null
|
||||
},
|
||||
|
||||
getSysCategoryById: (id: number) => {
|
||||
return get().sysCategories.find(c => c.id === id) || null
|
||||
},
|
||||
|
||||
updateCategories: (data, type) => {
|
||||
// 获取当前所有分类
|
||||
const allCategories = get().categories
|
||||
|
||||
// 保留另一类分类
|
||||
const otherCategories = allCategories.filter(c => c.type !== type)
|
||||
|
||||
// 给当前类型的数据重新计算 sort_number
|
||||
const updatedData = data.map((item, index) => ({
|
||||
...item,
|
||||
sort_number: index, // 按顺序重新赋值
|
||||
}))
|
||||
|
||||
// 合并另一类分类
|
||||
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)
|
||||
})
|
||||
|
||||
// 更新前端 state
|
||||
set({ categories: finalCategories })
|
||||
},
|
||||
|
||||
deleteCategoryById: async (id: number, type: number) => {
|
||||
const allCategories = get().categories
|
||||
|
||||
// 删除指定 id,同时只处理同类型的数据
|
||||
const finalCategories = allCategories
|
||||
.filter(c => c.id !== id)
|
||||
.map((item, index) => {
|
||||
if (item.type === type) {
|
||||
return { ...item, sort_number: index }
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
// 更新 state
|
||||
set({ categories: finalCategories })
|
||||
|
||||
// 调用删除接口
|
||||
await deleteMoneyCategoryApi({ id }).catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
// 同步更新排序给后端,仅同类型
|
||||
const payload = finalCategories
|
||||
.filter(c => c.type === type)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
sort_number: item.sort_number,
|
||||
}))
|
||||
await updateMoneyCategorySortApi({ data: payload })
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'money-category-storage',
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,190 @@
|
||||
import type { MoneyRecordGroup, MoneyRecordItem } from '@/types/money/record'
|
||||
import { create } from 'zustand'
|
||||
import { deleteMoneyRecordApi } from '@/request/api/money/record'
|
||||
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
|
||||
|
||||
setRecords: (records: MoneyRecordGroup[], total_expense: string, total_income: string, total_balance: string, month: string) => void
|
||||
setCurrentMonth: (month: string) => void
|
||||
|
||||
addMoneyRecord: (data: MoneyRecordItem) => void
|
||||
updateMoneyRecord: (updated: MoneyRecordItem) => void
|
||||
deleteMoneyRecord: (id: number) => void
|
||||
|
||||
getMoneyRecordById: (id: number) => MoneyRecordItem | null
|
||||
}
|
||||
|
||||
export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
|
||||
records: [],
|
||||
currentMonth: '',
|
||||
|
||||
total_income: '0.00',
|
||||
total_expense: '0.00',
|
||||
total_balance: '0.00',
|
||||
|
||||
setRecords: (records, total_expense, total_income, total_balance, month) => {
|
||||
set({ records, currentMonth: month, total_income, total_expense, total_balance })
|
||||
},
|
||||
|
||||
setCurrentMonth: month => set({ currentMonth: month }),
|
||||
|
||||
addMoneyRecord: (data) => {
|
||||
set((prev) => {
|
||||
if (!isInMonth(data.record_time, prev.currentMonth))
|
||||
return prev
|
||||
|
||||
const category = useMoneyCategoryStore.getState().getCategoryById(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,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
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().getCategoryById((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().getCategoryById((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) => {
|
||||
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().getCategoryById(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)
|
||||
},
|
||||
|
||||
getMoneyRecordById: (id: number) => {
|
||||
const records = get().records
|
||||
for (const group of records) {
|
||||
const record = group.records.find(r => r.id === id)
|
||||
if (record)
|
||||
return record
|
||||
}
|
||||
return null
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,21 @@
|
||||
import { router } from 'expo-router'
|
||||
// store/navigation.ts
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface NavState {
|
||||
pathname: string
|
||||
setPathname: (p: string) => void
|
||||
navigateToAuth: () => void
|
||||
}
|
||||
|
||||
export const useNavStore = create<NavState>(set => ({
|
||||
pathname: '/',
|
||||
setPathname: (p: string) => set({ pathname: p }),
|
||||
navigateToAuth: () => {
|
||||
const pathname = useNavStore.getState().pathname
|
||||
|
||||
if (!pathname.startsWith('/auth')) {
|
||||
router.replace('/auth')
|
||||
}
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,17 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface SpaceState {
|
||||
userSpace: UserSpace | null
|
||||
setUserSpace: (space: UserSpace | null) => void
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
export const useSpaceStore = create<SpaceState>(set => ({
|
||||
userSpace: null,
|
||||
setUserSpace: space => set({ userSpace: space }),
|
||||
clear: () => {
|
||||
set({
|
||||
userSpace: null,
|
||||
})
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,38 @@
|
||||
// store/userStore.ts
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { create } from 'zustand'
|
||||
import { createJSONStorage, persist } from 'zustand/middleware'
|
||||
|
||||
interface UserState {
|
||||
userInfo: UserInfo | null
|
||||
setUserInfo: (userInfo: UserInfo | null) => void
|
||||
clear: () => void
|
||||
isLoggedIn: () => boolean
|
||||
|
||||
userSpace: UserSpace | null
|
||||
setUserSpace: (userSpace: UserSpace | null) => void
|
||||
}
|
||||
|
||||
const useUserStore = create<UserState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
userInfo: null,
|
||||
userSpace: null,
|
||||
|
||||
setUserInfo: userInfo => set({ userInfo }),
|
||||
setUserSpace: userSpace => set({ userSpace }),
|
||||
|
||||
clear: () => {
|
||||
set({ userInfo: null, userSpace: null })
|
||||
},
|
||||
|
||||
isLoggedIn: () => !!get().userInfo?.token,
|
||||
}),
|
||||
{
|
||||
name: 'user-storage',
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
export { useUserStore }
|
||||
Reference in new issue
Block a user