u
This commit is contained in:
1 parent
49285f1df2
commit
63b168affc
7 files changed
+284
-324
No files matched your search
@@ -1,22 +1,21 @@
|
|||||||
import type { MoneyRecordGroup, MoneyRecordItem, QueryMoneyRecordsResult } from '@/types/money/record'
|
import type { MoneyRecordGroup, MoneyRecordItem, QueryMoneyRecordsResult } from '@/types/money/record'
|
||||||
import { useRouter } from 'expo-router'
|
import { useRouter } from 'expo-router'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Text, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native'
|
import { Text, TouchableOpacity, View } from 'react-native'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import { ADatePicker, LucideIcon } from '@/components/common/ao'
|
import { ADatePicker, LucideIcon } from '@/components/common/ao'
|
||||||
import { Fab, FabIcon } from '@/components/ui/fab'
|
import { Fab, FabIcon } from '@/components/ui/fab'
|
||||||
import { AddIcon } from '@/components/ui/icon'
|
import { AddIcon } from '@/components/ui/icon'
|
||||||
import { Progress, ProgressFilledTrack } from '@/components/ui/progress'
|
import { Progress, ProgressFilledTrack } from '@/components/ui/progress'
|
||||||
import { deleteMoneyRecordApi, queryMoneyRecordsApi } from '@/request/api/money/record'
|
import { deleteMoneyRecordApi, queryMoneyRecordsApi } from '@/request/api/money/record'
|
||||||
import { add, sub } from '@/utils/math'
|
import { useMoneyRecordStore } from '@/store/moneyRecord'
|
||||||
import { getCurrentYearMonth, isInMonth } from '@/utils/time'
|
import { getCurrentYearMonth } from '@/utils/time'
|
||||||
import { toast } from '@/utils/toast'
|
import { toast } from '@/utils/toast'
|
||||||
import AddMoneyDrawer from './components/AddDrawer'
|
|
||||||
import TransactionsList from './components/TransactionsList'
|
import TransactionsList from './components/TransactionsList'
|
||||||
|
|
||||||
export default function Money() {
|
export default function Money() {
|
||||||
|
const moneyRecordStore = useMoneyRecordStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [showAddDrawer, setShowAddDrawer] = useState(false)
|
|
||||||
// 安全高度
|
// 安全高度
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
|
|
||||||
@@ -25,22 +24,13 @@ export default function Money() {
|
|||||||
const [searchParams, setSearchParams] = useState({
|
const [searchParams, setSearchParams] = useState({
|
||||||
record_time: getCurrentYearMonth(),
|
record_time: getCurrentYearMonth(),
|
||||||
})
|
})
|
||||||
// 数据
|
|
||||||
const [moneyRecords, setMoneyRecords] = useState<QueryMoneyRecordsResult>({
|
|
||||||
total_expense: '0',
|
|
||||||
total_income: '0',
|
|
||||||
records: [],
|
|
||||||
})
|
|
||||||
// 更新
|
|
||||||
const [updateItem, setUpdateItem] = useState<MoneyRecordItem | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
queryMoneyRecordsApi({
|
queryMoneyRecordsApi({
|
||||||
record_time: searchParams.record_time,
|
record_time: searchParams.record_time,
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
console.log(res.data.records)
|
moneyRecordStore.setRecords(res.data.records, res.data.total_expense, res.data.total_income, searchParams.record_time)
|
||||||
setMoneyRecords(res.data)
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
toast.show({
|
toast.show({
|
||||||
@@ -52,232 +42,6 @@ export default function Money() {
|
|||||||
})
|
})
|
||||||
}, [searchParams])
|
}, [searchParams])
|
||||||
|
|
||||||
function handleAddMoneyRecord(data: MoneyRecordItem) {
|
|
||||||
const recordTime = data.record_time
|
|
||||||
|
|
||||||
setMoneyRecords((prev) => {
|
|
||||||
// 不在当前月份 → 不添加,直接返回
|
|
||||||
if (!isInMonth(recordTime, searchParams.record_time)) {
|
|
||||||
return prev
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = structuredClone(prev)
|
|
||||||
|
|
||||||
// 查找分组
|
|
||||||
const group = next.records.find(g => g.record_time === recordTime)
|
|
||||||
|
|
||||||
// === 已存在分组 ===
|
|
||||||
if (group) {
|
|
||||||
// 插入到最前面
|
|
||||||
group.records.unshift(data)
|
|
||||||
|
|
||||||
// 更新分组 & 总计
|
|
||||||
if (data.category_type === 1) {
|
|
||||||
// 收入
|
|
||||||
group.income = add(group.income, data.amount)
|
|
||||||
next.total_income = add(next.total_income, data.amount)
|
|
||||||
}
|
|
||||||
else if (data.category_type === 0) {
|
|
||||||
// 支出
|
|
||||||
group.expense = add(group.expense, data.amount)
|
|
||||||
next.total_expense = add(next.total_expense, data.amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
// === 无分组,新建 ===
|
|
||||||
const expense = data.category_type === 0 ? data.amount : '0'
|
|
||||||
const income = data.category_type === 1 ? data.amount : '0'
|
|
||||||
|
|
||||||
const newGroup: MoneyRecordGroup = {
|
|
||||||
record_time: recordTime,
|
|
||||||
income,
|
|
||||||
expense,
|
|
||||||
records: [data],
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 按日期(YYYY-MM-DD)倒序插入 ----
|
|
||||||
// 最新日期插入前面,比如:2025-01-05 > 2025-01-03
|
|
||||||
const index = next.records.findIndex(g => g.record_time < recordTime)
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
next.records.push(newGroup)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
next.records.splice(index, 0, newGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新总计
|
|
||||||
if (data.category_type === 0) {
|
|
||||||
next.total_income = add(next.total_income, data.amount)
|
|
||||||
}
|
|
||||||
else if (data.category_type === 1) {
|
|
||||||
next.total_expense = add(next.total_expense, data.amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUpdateMoneyRecord(updated: MoneyRecordItem) {
|
|
||||||
setMoneyRecords((prev) => {
|
|
||||||
const next = structuredClone(prev)
|
|
||||||
|
|
||||||
// 找到旧记录
|
|
||||||
const oldGroupIndex = next.records.findIndex(g =>
|
|
||||||
g.records.some(r => r.id === updated.id),
|
|
||||||
)
|
|
||||||
if (oldGroupIndex === -1)
|
|
||||||
return next
|
|
||||||
|
|
||||||
const oldGroup = next.records[oldGroupIndex]
|
|
||||||
const oldRecordIndex = oldGroup.records.findIndex(r => r.id === updated.id)
|
|
||||||
const oldRecord = oldGroup.records[oldRecordIndex]
|
|
||||||
|
|
||||||
const oldAmount = oldRecord.amount
|
|
||||||
const newAmount = updated.amount
|
|
||||||
const oldType = oldRecord.category_type
|
|
||||||
const newType = updated.category_type
|
|
||||||
const oldDate = oldRecord.record_time
|
|
||||||
const newDate = updated.record_time
|
|
||||||
|
|
||||||
// record_time 没变 → 同分组更新
|
|
||||||
if (oldDate === newDate) {
|
|
||||||
oldGroup.records[oldRecordIndex] = updated
|
|
||||||
|
|
||||||
// 调整分组金额
|
|
||||||
if (oldType === 1)
|
|
||||||
oldGroup.income = sub(oldGroup.income, oldAmount)
|
|
||||||
if (oldType === 0)
|
|
||||||
oldGroup.expense = sub(oldGroup.expense, oldAmount)
|
|
||||||
|
|
||||||
if (newType === 1)
|
|
||||||
oldGroup.income = add(oldGroup.income, newAmount)
|
|
||||||
if (newType === 0)
|
|
||||||
oldGroup.expense = add(oldGroup.expense, newAmount)
|
|
||||||
|
|
||||||
// 调整总计
|
|
||||||
if (oldType === 1)
|
|
||||||
next.total_income = sub(next.total_income, oldAmount)
|
|
||||||
if (oldType === 0)
|
|
||||||
next.total_expense = sub(next.total_expense, oldAmount)
|
|
||||||
|
|
||||||
if (newType === 1)
|
|
||||||
next.total_income = add(next.total_income, newAmount)
|
|
||||||
if (newType === 0)
|
|
||||||
next.total_expense = add(next.total_expense, newAmount)
|
|
||||||
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// 判断是否在当前月份内
|
|
||||||
// -------------------------
|
|
||||||
const currentMonth = searchParams.record_time // 例如 "2025-01"
|
|
||||||
|
|
||||||
const isInCurrentMonth = isInMonth(newDate, currentMonth)
|
|
||||||
|
|
||||||
// 1. 先移除旧分组 & 调整金额(无论是否在当前月份,都要做)
|
|
||||||
if (oldType === 1)
|
|
||||||
oldGroup.income = sub(oldGroup.income, oldAmount)
|
|
||||||
if (oldType === 0)
|
|
||||||
oldGroup.expense = sub(oldGroup.expense, oldAmount)
|
|
||||||
|
|
||||||
if (oldType === 1)
|
|
||||||
next.total_income = sub(next.total_income, oldAmount)
|
|
||||||
if (oldType === 0)
|
|
||||||
next.total_expense = sub(next.total_expense, oldAmount)
|
|
||||||
|
|
||||||
oldGroup.records.splice(oldRecordIndex, 1)
|
|
||||||
if (oldGroup.records.length === 0) {
|
|
||||||
next.records.splice(oldGroupIndex, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// 不在当前月份 → 不显示,直接返回 next
|
|
||||||
// -------------------------
|
|
||||||
if (!isInCurrentMonth) {
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------
|
|
||||||
// 在当前月份 → 移动到新分组
|
|
||||||
// -------------------------
|
|
||||||
|
|
||||||
// 找新分组,如果没有就创建
|
|
||||||
let newGroup = next.records.find(g => g.record_time === newDate)
|
|
||||||
if (!newGroup) {
|
|
||||||
newGroup = {
|
|
||||||
record_time: newDate,
|
|
||||||
income: '0.00',
|
|
||||||
expense: '0.00',
|
|
||||||
records: [],
|
|
||||||
}
|
|
||||||
// 按日期倒序插入
|
|
||||||
const insertIndex = next.records.findIndex(g => g.record_time < newDate)
|
|
||||||
if (insertIndex === -1)
|
|
||||||
next.records.push(newGroup)
|
|
||||||
else
|
|
||||||
next.records.splice(insertIndex, 0, newGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加到新分组
|
|
||||||
newGroup.records.unshift(updated)
|
|
||||||
|
|
||||||
// 加金额
|
|
||||||
if (newType === 1)
|
|
||||||
newGroup.income = add(newGroup.income, newAmount)
|
|
||||||
if (newType === 0)
|
|
||||||
newGroup.expense = add(newGroup.expense, newAmount)
|
|
||||||
|
|
||||||
if (newType === 1)
|
|
||||||
next.total_income = add(next.total_income, newAmount)
|
|
||||||
if (newType === 0)
|
|
||||||
next.total_expense = add(next.total_expense, newAmount)
|
|
||||||
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteRecord(id: number) {
|
|
||||||
// 直接更新本地状态
|
|
||||||
setMoneyRecords((prev) => {
|
|
||||||
const next = structuredClone(prev)
|
|
||||||
|
|
||||||
// 遍历每个分组
|
|
||||||
next.records.forEach((group) => {
|
|
||||||
const index = group.records.findIndex(r => r.id === id)
|
|
||||||
if (index !== -1) {
|
|
||||||
const record = group.records[index]
|
|
||||||
|
|
||||||
// 更新分组金额
|
|
||||||
if (record.category_type === 1) {
|
|
||||||
group.income = sub(group.income, record.amount)
|
|
||||||
next.total_income = sub(next.total_income, record.amount)
|
|
||||||
}
|
|
||||||
else if (record.category_type === 0) {
|
|
||||||
group.expense = sub(group.expense, record.amount)
|
|
||||||
next.total_expense = sub(next.total_expense, record.amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除记录
|
|
||||||
group.records.splice(index, 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 删除空分组
|
|
||||||
next.records = next.records.filter(g => g.records.length > 0)
|
|
||||||
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
|
|
||||||
// 异步请求删除 API
|
|
||||||
deleteMoneyRecordApi({ id }).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<View
|
<View
|
||||||
@@ -307,19 +71,7 @@ export default function Money() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="mt-3 m-4 ">
|
<View className="mt-3 m-4 ">
|
||||||
|
|
||||||
<View className="flex flex-row gap-4">
|
<View className="flex flex-row gap-4">
|
||||||
<View className="flex-1">
|
|
||||||
<Text className="text-gray-500 text-sm">余额</Text>
|
|
||||||
<Text
|
|
||||||
className="text-lg font-semibold mt-0.5"
|
|
||||||
numberOfLines={1}
|
|
||||||
ellipsizeMode="tail"
|
|
||||||
>
|
|
||||||
0.00
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<Text className="text-gray-500 text-sm">收入</Text>
|
<Text className="text-gray-500 text-sm">收入</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -327,7 +79,7 @@ export default function Money() {
|
|||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
ellipsizeMode="tail"
|
ellipsizeMode="tail"
|
||||||
>
|
>
|
||||||
0.00
|
{moneyRecordStore.total_income}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -338,7 +90,7 @@ export default function Money() {
|
|||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
ellipsizeMode="tail"
|
ellipsizeMode="tail"
|
||||||
>
|
>
|
||||||
0.00
|
{moneyRecordStore.total_expense}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -374,13 +126,15 @@ export default function Money() {
|
|||||||
|
|
||||||
<View className="flex-1 mt-2">
|
<View className="flex-1 mt-2">
|
||||||
<TransactionsList
|
<TransactionsList
|
||||||
records={moneyRecords.records}
|
records={moneyRecordStore.records}
|
||||||
onUpdate={(data) => {
|
onUpdate={(data) => {
|
||||||
setUpdateItem(data)
|
router.navigate({
|
||||||
setShowAddDrawer(true)
|
pathname: '/money/record/editor',
|
||||||
|
params: { id: data.id },
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
onDelete={(id) => {
|
onDelete={(id) => {
|
||||||
handleDeleteRecord(id)
|
moneyRecordStore.deleteMoneyRecord(id)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -388,35 +142,16 @@ export default function Money() {
|
|||||||
<Fab
|
<Fab
|
||||||
size="lg"
|
size="lg"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
// setShowAddDrawer(true)
|
|
||||||
router.navigate('/money/record/editor')
|
router.navigate('/money/record/editor')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FabIcon as={AddIcon} size="xl" />
|
<FabIcon as={AddIcon} size="xl" />
|
||||||
</Fab>
|
</Fab>
|
||||||
|
|
||||||
<AddMoneyDrawer
|
|
||||||
isOpen={showAddDrawer}
|
|
||||||
updateData={updateItem}
|
|
||||||
onClose={() => {
|
|
||||||
setShowAddDrawer(false)
|
|
||||||
setUpdateItem(null)
|
|
||||||
}}
|
|
||||||
onConfirm={(res) => {
|
|
||||||
setShowAddDrawer(false)
|
|
||||||
if (updateItem !== null) {
|
|
||||||
setUpdateItem(null)
|
|
||||||
handleUpdateMoneyRecord(res)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
handleAddMoneyRecord(res)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ADatePicker
|
<ADatePicker
|
||||||
show={showDatePicker}
|
show={showDatePicker}
|
||||||
mode="month"
|
mode="month"
|
||||||
|
date={searchParams.record_time}
|
||||||
onConfirm={(val) => {
|
onConfirm={(val) => {
|
||||||
setSearchParams({
|
setSearchParams({
|
||||||
...searchParams,
|
...searchParams,
|
||||||
|
|||||||
@@ -1,30 +1,29 @@
|
|||||||
import type { MoneyCategoryItem } from '@/types/money/category'
|
import type { MoneyCategoryItem } from '@/types/money/category'
|
||||||
import type { MoneyRecordBase, MoneyRecordItem } from '@/types/money/record'
|
import type { MoneyRecordBase, MoneyRecordItem } from '@/types/money/record'
|
||||||
import { useRouter } from 'expo-router'
|
import { useLocalSearchParams, useRouter } from 'expo-router'
|
||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Text, TouchableOpacity, View } from 'react-native'
|
import { Text, TouchableOpacity, View } from 'react-native'
|
||||||
import { LucideIcon } from '@/components/common/ao'
|
import { LucideIcon } from '@/components/common/ao'
|
||||||
import CustomHeader from '@/components/custom/Header'
|
import CustomHeader from '@/components/custom/Header'
|
||||||
import PagerTabs from '@/components/custom/PagerTabs'
|
import PagerTabs from '@/components/custom/PagerTabs'
|
||||||
|
import { insertMoneyRecordApi } from '@/request/api/money/record'
|
||||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||||
|
import { useMoneyRecordStore } from '@/store/moneyRecord'
|
||||||
import { getCurrentDate, toDateString } from '@/utils/time'
|
import { getCurrentDate, toDateString } from '@/utils/time'
|
||||||
import { toast } from '@/utils/toast'
|
import { toast } from '@/utils/toast'
|
||||||
import AmountKeyboard from './components/AmountKeyboard'
|
import AmountKeyboard from './components/AmountKeyboard'
|
||||||
import CategoryGrid from './components/CategoryGrid'
|
import CategoryGrid from './components/CategoryGrid'
|
||||||
|
|
||||||
export default function MoenyRecordEditor() {
|
export default function MoenyRecordEditor() {
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const moneyCategoryStore = useMoneyCategoryStore()
|
const moneyCategoryStore = useMoneyCategoryStore()
|
||||||
const expense = useMoneyCategoryStore(state => state.expense)
|
const moneyRecordStore = useMoneyRecordStore()
|
||||||
const income = useMoneyCategoryStore(state => state.income)
|
|
||||||
|
|
||||||
console.log(moneyCategoryStore.categories)
|
const router = useRouter()
|
||||||
console.log(expense)
|
const expense = moneyCategoryStore.getExpenseCategories()
|
||||||
console.log(income)
|
const income = moneyCategoryStore.getIncomeCategories()
|
||||||
|
|
||||||
|
const { id: updateId } = useLocalSearchParams<{ id: string }>()
|
||||||
|
|
||||||
// 选择顺序:0 支出 1收入
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
||||||
// 自定义键盘
|
// 自定义键盘
|
||||||
const [showKeydown, setShowKeydown] = useState(false)
|
const [showKeydown, setShowKeydown] = useState(false)
|
||||||
// 选中分类
|
// 选中分类
|
||||||
@@ -42,6 +41,25 @@ export default function MoenyRecordEditor() {
|
|||||||
|
|
||||||
const [moneyRecord, setMoneyRecord] = useState<MoneyRecordBase>(defaultMoneyRecord)
|
const [moneyRecord, setMoneyRecord] = useState<MoneyRecordBase>(defaultMoneyRecord)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (updateId) {
|
||||||
|
const updateData = moneyRecordStore.getMoneyRecordById(Number(updateId))
|
||||||
|
if (updateData !== null) {
|
||||||
|
setMoneyRecord({
|
||||||
|
...updateData,
|
||||||
|
record_time: toDateString(updateData.record_time),
|
||||||
|
})
|
||||||
|
setShowKeydown(true)
|
||||||
|
|
||||||
|
const moneyCategory = moneyCategoryStore.getCategoryById(updateData.category_id)
|
||||||
|
|
||||||
|
if (moneyCategory != null) {
|
||||||
|
setSelectedCategory(moneyCategory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 保存记录
|
// 保存记录
|
||||||
function handleSaveRecord() {
|
function handleSaveRecord() {
|
||||||
if (selectedCategory === null) {
|
if (selectedCategory === null) {
|
||||||
@@ -55,7 +73,39 @@ export default function MoenyRecordEditor() {
|
|||||||
toast.show({
|
toast.show({
|
||||||
title: '请输入金额',
|
title: '请输入金额',
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const amount = moneyRecord.amount.replace(/^-+/, '')
|
||||||
|
|
||||||
|
insertMoneyRecordApi({
|
||||||
|
...moneyRecord,
|
||||||
|
amount,
|
||||||
|
category_id: selectedCategory.id,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.isSuccess) {
|
||||||
|
const result: MoneyRecordItem = {
|
||||||
|
...res.data,
|
||||||
|
category_name: selectedCategory.name,
|
||||||
|
category_icon: selectedCategory.icon,
|
||||||
|
category_type: selectedCategory.type,
|
||||||
|
}
|
||||||
|
|
||||||
|
moneyRecordStore.addMoneyRecord(result)
|
||||||
|
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast.show({
|
||||||
|
title: res.message || '保存失败',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('发生异常啦')
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -70,6 +120,7 @@ export default function MoenyRecordEditor() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<PagerTabs
|
<PagerTabs
|
||||||
|
selectedIndex={selectedCategory?.type}
|
||||||
left={(
|
left={(
|
||||||
<TouchableOpacity className="w-[30%] flex items-center px-4 flex-row">
|
<TouchableOpacity className="w-[30%] flex items-center px-4 flex-row">
|
||||||
<LucideIcon icon="LayoutList" />
|
<LucideIcon icon="LayoutList" />
|
||||||
@@ -83,7 +134,7 @@ export default function MoenyRecordEditor() {
|
|||||||
router.back()
|
router.back()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text className="ml-auto text-gray-400">取消</Text>
|
<Text className="ml-auto text-base text-gray-400">取消</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
routes={[
|
routes={[
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface PagerTabsProps {
|
|||||||
routes: PagerRoute[]
|
routes: PagerRoute[]
|
||||||
left?: React.ReactNode
|
left?: React.ReactNode
|
||||||
right?: React.ReactNode
|
right?: React.ReactNode
|
||||||
initialIndex?: number
|
selectedIndex?: number // 外部控制选中页
|
||||||
onChange?: (key: string) => void
|
onChange?: (key: string) => void
|
||||||
indicatorWidth?: number
|
indicatorWidth?: number
|
||||||
|
|
||||||
@@ -25,17 +25,24 @@ export default function PagerTabs({
|
|||||||
routes,
|
routes,
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
initialIndex = 0,
|
selectedIndex,
|
||||||
onChange,
|
onChange,
|
||||||
indicatorWidth = 38,
|
indicatorWidth = 38,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
}: PagerTabsProps) {
|
}: PagerTabsProps) {
|
||||||
const pagerRef = React.useRef<PagerView>(null)
|
const pagerRef = React.useRef<PagerView>(null)
|
||||||
const [index, setIndex] = React.useState(initialIndex)
|
const [index, setIndex] = React.useState(selectedIndex ?? 0)
|
||||||
const [tabWidth, setTabWidth] = React.useState(0)
|
const [tabWidth, setTabWidth] = React.useState(0)
|
||||||
|
const scrollX = React.useRef(new Animated.Value(selectedIndex ?? 0)).current
|
||||||
|
|
||||||
const scrollX = React.useRef(new Animated.Value(0)).current
|
// 外部 selectedIndex 更新时同步内部 index
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (typeof selectedIndex === 'number' && selectedIndex !== index) {
|
||||||
|
setIndex(selectedIndex)
|
||||||
|
pagerRef.current?.setPage(selectedIndex)
|
||||||
|
}
|
||||||
|
}, [selectedIndex])
|
||||||
|
|
||||||
const onTabPress = (i: number) => {
|
const onTabPress = (i: number) => {
|
||||||
pagerRef.current?.setPage(i)
|
pagerRef.current?.setPage(i)
|
||||||
@@ -54,7 +61,6 @@ export default function PagerTabs({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tabWidthPerTab = tabWidth / routes.length
|
const tabWidthPerTab = tabWidth / routes.length
|
||||||
|
|
||||||
const indicatorTranslateX = scrollX.interpolate({
|
const indicatorTranslateX = scrollX.interpolate({
|
||||||
inputRange: routes.map((_, i) => i),
|
inputRange: routes.map((_, i) => i),
|
||||||
outputRange: routes.map(
|
outputRange: routes.map(
|
||||||
@@ -66,9 +72,7 @@ export default function PagerTabs({
|
|||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
className={`${className} flex-1`}
|
className={`${className} flex-1`}
|
||||||
style={[
|
style={[style]}
|
||||||
style,
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View className="flex flex-row items-center">
|
<View className="flex flex-row items-center">
|
||||||
@@ -86,7 +90,7 @@ export default function PagerTabs({
|
|||||||
{routes.map((r, i) => (
|
{routes.map((r, i) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={r.key}
|
key={r.key}
|
||||||
className="flex-1 items-center justify-center py-1"
|
className="flex-1 items-center justify-center py-2"
|
||||||
onPress={() => onTabPress(i)}
|
onPress={() => onTabPress(i)}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
@@ -115,7 +119,7 @@ export default function PagerTabs({
|
|||||||
<PagerView
|
<PagerView
|
||||||
ref={pagerRef}
|
ref={pagerRef}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
initialPage={initialIndex}
|
initialPage={selectedIndex ?? 0}
|
||||||
onPageScroll={onPageScroll}
|
onPageScroll={onPageScroll}
|
||||||
onPageSelected={e => onPageSelected(e.nativeEvent.position)}
|
onPageSelected={e => onPageSelected(e.nativeEvent.position)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ function isWhiteListed(url: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const alovaInstance = createAlova({
|
const alovaInstance = createAlova({
|
||||||
// baseURL: 'http://192.168.5.6:8085/app',
|
baseURL: 'http://192.168.5.6:8085/app',
|
||||||
baseURL: 'http://192.168.1.136:8085/app',
|
// baseURL: 'http://192.168.1.136:8085/app',
|
||||||
requestAdapter: axiosRequestAdapter(),
|
requestAdapter: axiosRequestAdapter(),
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
cacheFor: null,
|
cacheFor: null,
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import { queryMoneyCategoryApi } from '@/request/api/money/category'
|
|||||||
interface MoneyCategoryStore {
|
interface MoneyCategoryStore {
|
||||||
categories: MoneyCategoryItem[]
|
categories: MoneyCategoryItem[]
|
||||||
|
|
||||||
// 通过 getter 动态计算
|
|
||||||
expense: MoneyCategoryItem[]
|
|
||||||
income: MoneyCategoryItem[]
|
|
||||||
|
|
||||||
setCategories: (categories: MoneyCategoryItem[]) => void
|
setCategories: (categories: MoneyCategoryItem[]) => void
|
||||||
addCategory: (category: MoneyCategoryItem) => void
|
addCategory: (category: MoneyCategoryItem) => void
|
||||||
removeCategory: (id: number) => void
|
removeCategory: (id: number) => void
|
||||||
init: () => Promise<void>
|
init: () => Promise<void>
|
||||||
|
|
||||||
|
getExpenseCategories: () => MoneyCategoryItem[]
|
||||||
|
getIncomeCategories: () => MoneyCategoryItem[]
|
||||||
|
|
||||||
|
getCategoryById: (id: number) => MoneyCategoryItem | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
||||||
@@ -22,34 +23,32 @@ export const useMoneyCategoryStore = create<MoneyCategoryStore>()(
|
|||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
categories: [],
|
categories: [],
|
||||||
|
|
||||||
// getter 样式属性,通过 get() 动态计算
|
|
||||||
get expense() {
|
|
||||||
return get().categories.filter(c => c.type === 0)
|
|
||||||
},
|
|
||||||
get income() {
|
|
||||||
return get().categories.filter(c => c.type === 1)
|
|
||||||
},
|
|
||||||
|
|
||||||
setCategories: categories => set({ categories }),
|
setCategories: categories => set({ categories }),
|
||||||
|
|
||||||
addCategory: category =>
|
addCategory: category =>
|
||||||
set(state => ({ categories: [...state.categories, category] })),
|
set({ categories: [...get().categories, category] }),
|
||||||
|
|
||||||
removeCategory: id =>
|
removeCategory: id =>
|
||||||
set(state => ({ categories: state.categories.filter(c => c.id !== id) })),
|
set({ categories: get().categories.filter(c => c.id !== id) }),
|
||||||
|
|
||||||
init: async () => {
|
init: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await queryMoneyCategoryApi()
|
const res = await queryMoneyCategoryApi()
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
const categories = res.data || []
|
set({ categories: res.data || [] })
|
||||||
set({ categories })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (e) {
|
||||||
console.error('Failed to fetch categories:', error)
|
console.error('Fetch category failed:', e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'money-category-storage',
|
name: 'money-category-storage',
|
||||||
|
|||||||
@@ -1,3 +1,172 @@
|
|||||||
|
import type { MoneyRecordGroup, MoneyRecordItem } from '@/types/money/record'
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { MoneyCategoryItem } from '@/types/money/category'
|
import { deleteMoneyRecordApi } from '@/request/api/money/record'
|
||||||
import { MoneyRecordGroup, MoneyRecordItem } from '@/types/money/record'
|
import { add, sub } from '@/utils/math'
|
||||||
|
import { isInMonth } from '@/utils/time'
|
||||||
|
|
||||||
|
interface MoneyRecordStore {
|
||||||
|
records: MoneyRecordGroup[]
|
||||||
|
currentMonth: string
|
||||||
|
|
||||||
|
total_income: string
|
||||||
|
total_expense: string
|
||||||
|
|
||||||
|
setRecords: (records: MoneyRecordGroup[], total_expense: string, total_income: 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',
|
||||||
|
|
||||||
|
setRecords: (records, total_expense, total_income, month) => {
|
||||||
|
set({ records, currentMonth: month, total_income, total_expense })
|
||||||
|
},
|
||||||
|
|
||||||
|
setCurrentMonth: month => set({ currentMonth: month }),
|
||||||
|
|
||||||
|
addMoneyRecord: (data) => {
|
||||||
|
set((prev) => {
|
||||||
|
if (!isInMonth(data.record_time, prev.currentMonth))
|
||||||
|
return prev
|
||||||
|
|
||||||
|
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 (data.category_type === 0)
|
||||||
|
group.expense = add(group.expense, amount)
|
||||||
|
if (data.category_type === 1)
|
||||||
|
group.income = add(group.income, amount)
|
||||||
|
records[groupIndex] = group
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const newGroup: MoneyRecordGroup = {
|
||||||
|
record_time: data.record_time,
|
||||||
|
records: [data],
|
||||||
|
expense: data.category_type === 0 ? amount : '0.00',
|
||||||
|
income: data.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
records,
|
||||||
|
total_expense: data.category_type === 0 ? add(prev.total_expense, amount) : prev.total_expense,
|
||||||
|
total_income: data.category_type === 1 ? add(prev.total_income, amount) : prev.total_income,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
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 oldGroup = { ...records[oldGroupIndex], records: [...records[oldGroupIndex].records] }
|
||||||
|
const oldRecordIndex = oldGroup.records.findIndex(r => r.id === updated.id)
|
||||||
|
const oldRecord = oldGroup.records[oldRecordIndex]
|
||||||
|
|
||||||
|
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 (oldRecord.category_type === 0)
|
||||||
|
total_expense = sub(total_expense, oldAmount)
|
||||||
|
if (oldRecord.category_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 (updated.category_type === 0)
|
||||||
|
group.expense = add(group.expense, newAmount)
|
||||||
|
if (updated.category_type === 1)
|
||||||
|
group.income = add(group.income, newAmount)
|
||||||
|
records[newGroupIndex] = group
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const newGroup: MoneyRecordGroup = {
|
||||||
|
record_time: updated.record_time,
|
||||||
|
records: [updated],
|
||||||
|
expense: updated.category_type === 0 ? newAmount : '0.00',
|
||||||
|
income: updated.category_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 (updated.category_type === 0)
|
||||||
|
total_expense = add(total_expense, newAmount)
|
||||||
|
if (updated.category_type === 1)
|
||||||
|
total_income = add(total_income, newAmount)
|
||||||
|
|
||||||
|
return { ...prev, records, total_expense, total_income }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
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]
|
||||||
|
if (record.category_type === 0)
|
||||||
|
total_expense = sub(total_expense, record.amount)
|
||||||
|
if (record.category_type === 1)
|
||||||
|
total_income = sub(total_income, record.amount)
|
||||||
|
group.records.splice(index, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const newRecords = records.filter(g => g.records.length > 0)
|
||||||
|
return { ...prev, records: newRecords, total_expense, total_income }
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
}))
|
||||||
@@ -64,6 +64,8 @@ func queryMoneyRecords(c *router.Context) error {
|
|||||||
|
|
||||||
recordMonth := bodyData.GetString("record_time")
|
recordMonth := bodyData.GetString("record_time")
|
||||||
|
|
||||||
|
fmt.Println(recordMonth)
|
||||||
|
|
||||||
// 解析年月
|
// 解析年月
|
||||||
startTime, err := time.Parse("2006-01", recordMonth)
|
startTime, err := time.Parse("2006-01", recordMonth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -82,7 +84,7 @@ func queryMoneyRecords(c *router.Context) error {
|
|||||||
muc.type AS category_type
|
muc.type AS category_type
|
||||||
FROM money_record mr
|
FROM money_record mr
|
||||||
LEFT JOIN money_user_category muc ON mr.category_id = muc.id
|
LEFT JOIN money_user_category muc ON mr.category_id = muc.id
|
||||||
WHERE mr.create_time BETWEEN '%s' AND '%s'
|
WHERE mr.record_time BETWEEN '%s' AND '%s'
|
||||||
ORDER BY mr.record_time DESC, mr.create_time DESC
|
ORDER BY mr.record_time DESC, mr.create_time DESC
|
||||||
`, startTime.Format("2006-01-02 15:04:05"), endTime.Format("2006-01-02 15:04:05"))
|
`, startTime.Format("2006-01-02 15:04:05"), endTime.Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user