u
This commit is contained in:
1 parent
ff3ee944d5
commit
bde3487da3
4 files changed
+801
-9
No files matched your search
@@ -0,0 +1,193 @@
|
||||
import type { MoneyCategoryItem } from '@/types/money/category'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import {
|
||||
Actionsheet,
|
||||
ActionsheetBackdrop,
|
||||
ActionsheetContent,
|
||||
ActionsheetDragIndicator,
|
||||
ActionsheetDragIndicatorWrapper,
|
||||
ActionsheetItem,
|
||||
ActionsheetItemText,
|
||||
ActionsheetSectionHeaderText,
|
||||
ActionsheetSectionList,
|
||||
} from '@/components/ui/actionsheet'
|
||||
|
||||
interface CategorySection {
|
||||
title: string
|
||||
data: MoneyCategoryItem[]
|
||||
}
|
||||
|
||||
interface CategorySheetProps {
|
||||
open?: boolean
|
||||
moneyCategories?: MoneyCategoryItem[]
|
||||
value?: number | null
|
||||
onClose?: () => void
|
||||
onSelect?: (category: MoneyCategoryItem) => void
|
||||
}
|
||||
|
||||
export default function CategorySheet({
|
||||
open = false,
|
||||
moneyCategories = [],
|
||||
value,
|
||||
onClose,
|
||||
onSelect,
|
||||
}: CategorySheetProps) {
|
||||
const listRef = useRef<any>(null)
|
||||
const hasAutoScrolled = useRef(false)
|
||||
|
||||
// 记住目标滚动位置
|
||||
const pendingScrollRef = useRef<{
|
||||
sectionIndex: number
|
||||
itemIndex: number
|
||||
} | null>(null)
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(item: MoneyCategoryItem) => {
|
||||
onSelect?.(item)
|
||||
onClose?.()
|
||||
},
|
||||
[onSelect, onClose],
|
||||
)
|
||||
|
||||
function createVirtualCategory(
|
||||
id: number,
|
||||
name: string,
|
||||
type: number,
|
||||
): MoneyCategoryItem {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
icon: '',
|
||||
sort_number: -1,
|
||||
}
|
||||
}
|
||||
|
||||
const sections = useMemo<CategorySection[]>(() => {
|
||||
const expense = moneyCategories.filter(i => i.type === 0)
|
||||
const income = moneyCategories.filter(i => i.type === 1)
|
||||
|
||||
return [
|
||||
{
|
||||
title: '筛选',
|
||||
data: [
|
||||
createVirtualCategory(-1, '全部', -1),
|
||||
createVirtualCategory(-2, '支出', 0),
|
||||
createVirtualCategory(-3, '收入', 1),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '支出分类',
|
||||
data: expense,
|
||||
},
|
||||
{
|
||||
title: '收入分类',
|
||||
data: income,
|
||||
},
|
||||
]
|
||||
}, [moneyCategories])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
hasAutoScrolled.current = false
|
||||
pendingScrollRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
if (hasAutoScrolled.current)
|
||||
return
|
||||
if (value == null)
|
||||
return
|
||||
|
||||
let sectionIndex = -1
|
||||
let itemIndex = -1
|
||||
|
||||
sections.some((section, sIndex) => {
|
||||
const index = section.data.findIndex(i => i.id === value)
|
||||
if (index !== -1) {
|
||||
sectionIndex = sIndex
|
||||
itemIndex = index
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (sectionIndex !== -1 && itemIndex !== -1) {
|
||||
// 记录目标位置,供失败兜底使用
|
||||
pendingScrollRef.current = { sectionIndex, itemIndex }
|
||||
requestAnimationFrame(() => {
|
||||
listRef.current?.scrollToLocation({
|
||||
sectionIndex,
|
||||
itemIndex,
|
||||
animated: true,
|
||||
viewPosition: 0.5,
|
||||
})
|
||||
})
|
||||
|
||||
hasAutoScrolled.current = true
|
||||
}
|
||||
}, [open, value, sections])
|
||||
|
||||
return (
|
||||
<Actionsheet isOpen={open} onClose={onClose}>
|
||||
<ActionsheetBackdrop />
|
||||
<ActionsheetContent style={{ maxHeight: '60%' }}>
|
||||
<ActionsheetDragIndicatorWrapper>
|
||||
<ActionsheetDragIndicator />
|
||||
</ActionsheetDragIndicatorWrapper>
|
||||
|
||||
<ActionsheetSectionList
|
||||
ref={listRef}
|
||||
sections={sections}
|
||||
stickySectionHeadersEnabled={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onEndReachedThreshold={0.1}
|
||||
onScrollToIndexFailed={() => {
|
||||
const target = pendingScrollRef.current
|
||||
if (!target)
|
||||
return
|
||||
|
||||
setTimeout(() => {
|
||||
listRef.current?.scrollToLocation({
|
||||
sectionIndex: target.sectionIndex,
|
||||
itemIndex: target.itemIndex,
|
||||
animated: true,
|
||||
viewPosition: 0.5,
|
||||
})
|
||||
}, 0)
|
||||
}}
|
||||
keyExtractor={(item, index) =>
|
||||
String((item as MoneyCategoryItem).id ?? index)}
|
||||
renderItem={({ item }) => {
|
||||
const category = item as MoneyCategoryItem
|
||||
const selected = value === category.id
|
||||
|
||||
return (
|
||||
<ActionsheetItem
|
||||
className={selected ? 'bg-gray-100' : ''}
|
||||
onPress={() => handleSelect(category)}
|
||||
>
|
||||
<ActionsheetItemText>
|
||||
{category.name}
|
||||
</ActionsheetItemText>
|
||||
</ActionsheetItem>
|
||||
)
|
||||
}}
|
||||
renderSectionHeader={({ section }) => {
|
||||
const s = section as CategorySection
|
||||
|
||||
return (
|
||||
<ActionsheetSectionHeaderText className="font-semibold text-[black] text-sm bg-white">
|
||||
{s.title}
|
||||
{' '}
|
||||
(
|
||||
{s.data.length}
|
||||
)
|
||||
</ActionsheetSectionHeaderText>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</ActionsheetContent>
|
||||
</Actionsheet>
|
||||
)
|
||||
}
|
||||
@@ -6,13 +6,16 @@ import { ADatePicker, ADrawer, AListItem, LucideIcon } from '@/components/common
|
||||
import { Fab, FabIcon } from '@/components/ui/fab'
|
||||
import { AddIcon } from '@/components/ui/icon'
|
||||
import { deleteMoneyRecordApi, listMoneyRecordsApi } from '@/request/api/money/record'
|
||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||
import { useMoneyRecordStore } from '@/store/moneyRecord'
|
||||
import { getCurrentYearMonth } from '@/utils/time'
|
||||
import { toast } from '@/utils/toast'
|
||||
import CategorySheet from './components/CategorySheet'
|
||||
import TransactionsList from './components/TransactionsList'
|
||||
|
||||
export default function Money() {
|
||||
const moneyRecordStore = useMoneyRecordStore()
|
||||
const moneyCategoryStore = useMoneyCategoryStore()
|
||||
const router = useRouter()
|
||||
// 安全高度
|
||||
const insets = useSafeAreaInsets()
|
||||
@@ -21,7 +24,9 @@ export default function Money() {
|
||||
const [showDatePicker, setShowDatePicker] = useState(false)
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
record_time: getCurrentYearMonth(),
|
||||
category: -1,
|
||||
})
|
||||
const [showCategorySheet, setShowCategorySheet] = useState(false)
|
||||
|
||||
// 操作菜单
|
||||
const [showActionsMenu, setShowActionsMenu] = useState(false)
|
||||
@@ -68,13 +73,23 @@ export default function Money() {
|
||||
}}
|
||||
>
|
||||
<View className="flex flex-row justify-between items-center px-3">
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowDatePicker(true)}
|
||||
className="flex flex-row items-center gap-1"
|
||||
>
|
||||
<Text className="font-semibold text-lg">{searchParams.record_time}</Text>
|
||||
<LucideIcon icon="ChevronDown" size={16} color="#666" clickable={false} />
|
||||
</TouchableOpacity>
|
||||
<View className="flex flex-row items-center gap-2">
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowDatePicker(true)}
|
||||
className="flex flex-row items-center gap-1"
|
||||
>
|
||||
<Text className="font-semibold text-lg">{searchParams.record_time}</Text>
|
||||
<LucideIcon icon="ChevronDown" size={16} color="#666" clickable={false} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowCategorySheet(true)}
|
||||
className="flex flex-row items-center gap-1"
|
||||
>
|
||||
<Text className="font-semibold text-lg">分类</Text>
|
||||
<LucideIcon icon="ChevronDown" size={16} color="#666" clickable={false} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
className="flex flex-row items-center gap-2"
|
||||
@@ -181,6 +196,21 @@ export default function Money() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<CategorySheet
|
||||
open={showCategorySheet}
|
||||
value={searchParams.category}
|
||||
moneyCategories={moneyCategoryStore.categories}
|
||||
onClose={() => {
|
||||
setShowCategorySheet(false)
|
||||
}}
|
||||
onSelect={(category) => {
|
||||
setSearchParams(prev => ({
|
||||
...prev,
|
||||
category: category.id,
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
|
||||
<ADrawer
|
||||
visible={showActionsMenu}
|
||||
hideModal={() => {
|
||||
|
||||
Reference in new issue
Block a user