This commit is contained in:
oneao committed 2026-01-14 17:31:20 +08:00
1 parent d4082a54d5
commit cb4562361a
8 files changed
+281 -144

No files matched your search

@@ -0,0 +1,105 @@
import { useEffect, useState } from 'react'
import { Pressable, ScrollView, Text, View } from 'react-native'
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
import {
Drawer,
DrawerBackdrop,
DrawerBody,
DrawerContent,
DrawerHeader,
} from '@/components/ui/drawer'
import { Input, InputField } from '@/components/ui/input'
import { useMoneyCategoryStore } from '@/store/moneyCategory'
interface CreateCategoryProps {
open: boolean
onClose?: () => void
}
export default function CreateCategory({
open,
onClose,
}: CreateCategoryProps) {
const moneyCategoryStore = useMoneyCategoryStore()
const [isOpen, setIsOpen] = useState(open)
useEffect(() => {
setIsOpen(open)
}, [open])
function handleClose() {
setIsOpen(false)
onClose && onClose()
}
return (
<Drawer isOpen={isOpen} onClose={handleClose} size="lg" anchor="bottom">
<DrawerBackdrop />
<DrawerContent
style={{
paddingLeft: 0,
paddingRight: 0,
}}
>
<DrawerHeader className="px-4 flex flex-col gap-4">
<View className="flex flex-row items-center justify-between w-full">
<Pressable>
<Text className="text-gray-600">取消</Text>
</Pressable>
<Text className="text-xl font-medium">类别</Text>
<Pressable>
<Text className="text-gray-600">确认</Text>
</Pressable>
</View>
<Input
variant="outline"
>
<InputField placeholder="请输入名称" />
</Input>
</DrawerHeader>
<DrawerBody>
<ScrollView className="flex-1">
{Object.entries(moneyCategoryStore.ICON_GROUPS).map(([groupName, icons]) => (
<View key={groupName} className="mb-4">
<Text className="text-center mb-4">
{groupName}
</Text>
<View className="flex flex-row flex-wrap">
{icons.map(icon => (
<Pressable
key={icon}
className="w-1/5 mb-3 flex items-center"
onPress={() => {
console.log('select icon:', icon)
}}
>
<View
style={{
width: 46,
height: 46,
borderRadius: 28,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F3F4F6',
}}
>
<MoneyCategoryIcon name={icon} size={28} />
</View>
</Pressable>
))}
</View>
</View>
))}
</ScrollView>
</DrawerBody>
</DrawerContent>
</Drawer>
)
}
@@ -9,6 +9,7 @@ import { AButton, ATab, LucideIcon } from '@/components/common/ao'
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
import CustomHeader from '@/components/custom/Header'
import { useMoneyCategoryStore } from '@/store/moneyCategory'
import CreateCategory from './components/CreateCategory'
export default function MoneyCategorySetting() {
const [selectedTab, setSelectedTab] = useState(0)
@@ -21,6 +22,8 @@ export default function MoneyCategorySetting() {
const [draggingDeleteZone, setDraggingDeleteZone] = useState<boolean>(false)
const [deleteTimer, setDeleteTimer] = useState<number | null>(null)
const [openCreateCategory, setOpenCreateCategory] = useState<boolean>(false)
useEffect(() => {
setCategories(selectedTab === 0 ? moneyCategoryStore.getExpenseCategories() : moneyCategoryStore.getIncomeCategories())
}, [selectedTab])
@@ -90,6 +93,9 @@ export default function MoneyCategorySetting() {
color: '#fff',
clickable: false,
}}
onPress={(() =>
setOpenCreateCategory(true)
)}
>
新增
</AButton>
@@ -114,14 +120,16 @@ export default function MoneyCategorySetting() {
Vibration.vibrate(50)
setDraggingId(data.key)
}}
onDragEnd={(data) => {
onDragEnd={(dragData) => {
setDraggingId('')
if (draggingDeleteZone)
return
const newOrder = data.data
const newData = dragData.data
moneyCategoryStore.updateCategories(newOrder, selectedTab)
if (JSON.stringify(categories) !== JSON.stringify(newData)) {
moneyCategoryStore.updateSortNumbers(newData, selectedTab)
}
}}
/>
</Animated.ScrollView>
@@ -139,7 +147,7 @@ export default function MoneyCategorySetting() {
const deleteId = Number(draggingId)
setCategories(prev => prev.filter(c => c.id !== deleteId))
moneyCategoryStore.deleteCategoryById(deleteId, selectedTab)
moneyCategoryStore.deleteCategoryById(deleteId)
setDraggingId('')
setDraggingDeleteZone(false)
@@ -179,6 +187,11 @@ export default function MoneyCategorySetting() {
</Sortable.MultiZoneProvider>
</View>
</View>
<CreateCategory
open={openCreateCategory}
onClose={() => setOpenCreateCategory(false)}
/>
</View>
)
}
@@ -18,7 +18,7 @@ export default function PagerWithTab() {
const fetchData = async () => {
await Promise.all([
getUserSpaceApi({})
getUserSpaceApi()
.then((res) => {
if (res.isSuccess) {
if (res.data === null) {
@@ -1,8 +1,6 @@
import type {
ExitSpaceParams,
ExitSpaceResult,
GetUserSpaceParams,
GetUserSpaceResult,
JoinSpaceParams,
JoinSpaceResult,
ListSpaceUsersParams,
@@ -14,8 +12,8 @@ import alovaInstance from '../../index'
const API_PREFIX = '/space/user'
export function getUserSpaceApi(params: GetUserSpaceParams) {
return alovaInstance.Get<ResponseData<GetUserSpaceResult | null>>(`${API_PREFIX}/get`, params)
export function getUserSpaceApi() {
return alovaInstance.Get<ResponseData<UserSpace | null>>(`${API_PREFIX}/get`)
}
export function exitSpaceApi(params: ExitSpaceParams) {
+1 -2
View File
@@ -18,7 +18,6 @@ export const useAppStore = create<AppState>((set, get) => ({
setReady: (v: boolean) => set({ ready: v }),
init: async () => {
// 如果已经初始化过就不重复执行(除非你手动 setReady(false) 再 init)
if (get().ready)
return
@@ -40,7 +39,7 @@ export const useAppStore = create<AppState>((set, get) => ({
await moneyCategoryStore.init()
// 获取空间信息
const res = await getUserSpaceApi({})
const res = await getUserSpaceApi()
spaceStore.setUserSpace(res.data)
}
catch (err) {
+151 -118
View File
@@ -1,147 +1,180 @@
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 {
ICON_GROUPS: Record<string, string[]>
categories: MoneyCategoryItem[]
init: () => Promise<void>
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
updateSortNumbers: (data: MoneyCategoryItem[], type: number) => void
deleteCategoryById: (id: number) => void
clear: () => void
}
export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
persist(
(set, get) => ({
categories: [],
sysCategories: [],
(set, get) => ({
ICON_GROUPS: {
餐饮: [
'money_canyin',
'money_yinpin',
'money_lingshi',
'money_xiaoye',
],
娱乐: [
'money_yule',
'money_youxi',
'money_dianying',
],
交通: [
'money_jiaotong',
'money_tingchefei',
'money_jiayou',
],
生活消费: [
'money_lijin',
'money_jiadian',
'money_riyongpin',
'money_shucai',
'money_meifa',
'money_shuidian',
'money_wenju',
'money_zhufang',
'money_baoxian',
'money_baojianpin',
'money_huafei',
'money_lvyou',
'money_gouwu',
'money_shuben',
'money_chongwu',
'money_ruanjian',
],
健康: [
'money_tijian',
'money_yao',
'money_jiuyi',
],
公益: [
'money_gongyi',
],
兼职与收入: [
'money_gongzi',
'money_jianzhi',
],
鲜花与水果: [
'money_shuiguo',
'money_xianhua',
],
转账与转移: [
'money_zhuanzhang',
],
家庭与家电: [
'money_kuaidi',
'money_weixiu',
'money_jiaju',
'money_muying',
'money_liwu',
],
运动与娱乐: [
'money_yundong',
'money_peixun',
],
},
setCategories: categories => set({ categories }),
setSysCategories: categories => set({ categories }),
categories: [],
addCategory: category =>
set({ categories: [...get().categories, category] }),
setCategories: categories => set({ categories }),
removeCategory: id =>
set({ categories: get().categories.filter(c => c.id !== id) }),
addCategory: category =>
set({ categories: [...get().categories, category] }),
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)
init: async () => {
try {
await queryMoneyCategoryApi().then((res) => {
if (res.isSuccess) {
set({ categories: res.data.categories || [] })
}
}).catch((err) => {
console.log(err)
}).finally(() => {
})
// 更新前端 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),
}
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
},
updateSortNumbers: (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) => {
const allCategories = get().categories
// 删除指定 id,同时只处理同类型的数据
const finalCategories = allCategories
.filter(c => c.id !== id)
// 更新 state
set({ categories: finalCategories })
// 调用删除接口
await deleteMoneyCategoryApi({ id }).catch((err) => {
console.log(err)
})
},
}),
)
@@ -42,7 +42,7 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
if (!isInMonth(data.record_time, prev.currentMonth))
return prev
const category = useMoneyCategoryStore.getState().getCategoryById(data.category_id)
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
@@ -88,12 +88,12 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
if (oldGroupIndex === -1)
return prev
const updateRecordCategory = useMoneyCategoryStore.getState().getCategoryById((updated.category_id))
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().getCategoryById((oldRecord.category_id))
const oldRecordCategory = useMoneyCategoryStore.getState().getById((oldRecord.category_id))
const oldAmount = oldRecord.amount
const newAmount = updated.amount
@@ -158,7 +158,7 @@ export const useMoneyRecordStore = create<MoneyRecordStore>((set, get) => ({
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)
const category = useMoneyCategoryStore.getState().getById(record.category_id)
if (category?.id === 0)
total_expense = sub(total_expense, record.amount)
@@ -1,14 +1,3 @@
// 获取用户的空间
export interface GetUserSpaceParams {
}
export interface GetUserSpaceResult {
space_id: number
space_name: string
role: number
invite_code: string
}
// 退出空间
export interface ExitSpaceParams {