This commit is contained in:
oneao committed 2026-02-02 22:42:58 +08:00
1 parent bde3487da3
commit 7a9a22e02e
9 files changed
+243 -60

No files matched your search

+1
View File
@@ -18,6 +18,7 @@
"@legendapp/list": "^2.0.19", "@legendapp/list": "^2.0.19",
"@legendapp/motion": "2.4.0", "@legendapp/motion": "2.4.0",
"@react-native-async-storage/async-storage": "2.2.0", "@react-native-async-storage/async-storage": "2.2.0",
"@react-native-segmented-control/segmented-control": "^2.5.7",
"@react-navigation/drawer": "^7.5.0", "@react-navigation/drawer": "^7.5.0",
"@yz1311/react-native-wheel-picker": "^0.4.5", "@yz1311/react-native-wheel-picker": "^0.4.5",
"alova": "^3.3.4", "alova": "^3.3.4",
+14
View File
@@ -32,6 +32,9 @@ importers:
'@react-native-async-storage/async-storage': '@react-native-async-storage/async-storage':
specifier: 2.2.0 specifier: 2.2.0
version: 2.2.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)) version: 2.2.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))
'@react-native-segmented-control/segmented-control':
specifier: ^2.5.7
version: 2.5.7(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@react-navigation/drawer': '@react-navigation/drawer':
specifier: ^7.5.0 specifier: ^7.5.0
version: 7.7.9(@react-navigation/native@7.1.19(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) version: 7.7.9(@react-navigation/native@7.1.19(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -1702,6 +1705,12 @@ packages:
peerDependencies: peerDependencies:
react-native: ^0.0.0-0 || >=0.65 <1.0 react-native: ^0.0.0-0 || >=0.65 <1.0
'@react-native-segmented-control/segmented-control@2.5.7':
resolution: {integrity: sha512-l84YeVX8xAU3lvOJSvV4nK/NbGhIm2gBfveYolwaoCbRp+/SLXtc6mYrQmM9ScXNwU14mnzjQTpTHWl5YPnkzQ==}
peerDependencies:
react: '>=16.0'
react-native: '>=0.62'
'@react-native/assets-registry@0.81.5': '@react-native/assets-registry@0.81.5':
resolution: {integrity: sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w==} resolution: {integrity: sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w==}
engines: {node: '>= 20.19.4'} engines: {node: '>= 20.19.4'}
@@ -8278,6 +8287,11 @@ snapshots:
merge-options: 3.0.4 merge-options: 3.0.4
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
'@react-native-segmented-control/segmented-control@2.5.7(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
dependencies:
react: 19.1.0
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
'@react-native/assets-registry@0.81.5': {} '@react-native/assets-registry@0.81.5': {}
'@react-native/babel-plugin-codegen@0.81.5(@babel/core@7.28.5)': '@react-native/babel-plugin-codegen@0.81.5(@babel/core@7.28.5)':
+79 -34
View File
@@ -1,40 +1,85 @@
import { DateRangePicker } from '@yz1311/react-native-wheel-picker' import SegmentedControl from '@react-native-segmented-control/segmented-control'
import React from 'react' import React, { useState } from 'react'
import { Button, ButtonText } from '@/components/ui/button' import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { ATab } from '@/components/common/ao'
export default function DemoDatePickerUI() {
const [index, setIndex] = useState(0)
export default function Example() {
const [showDrawer, setShowDrawer] = React.useState(false)
return ( return (
<> <View style={{ flex: 1, backgroundColor: '#f3f4f6', padding: 16 }}>
<Button {/* 快捷选项栏 */}
onPress={() => { <ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ marginBottom: 12 }}>
setShowDrawer(true) <ATab
}} data={[
className="mt-10" {
> label: '不限',
<ButtonText>Open Drawer</ButtonText> key: '不限',
</Button> },
{
label: '今天',
key: '今天',
},
{
label: '本月',
key: '本月',
},
{
label: '本年',
key: '本年',
},
]}
/>
</ScrollView>
<DateRangePicker {/* SegmentedControl */}
pickerTitle="预约时间" <SegmentedControl
pickerCancelBtnText="取消" values={['时间', '日', '月', '年', '范围']}
pickerConfirmBtnText="确认" selectedIndex={index}
// 开启modal模式 onChange={event => setIndex(event.nativeEvent.selectedSegmentIndex)}
isModal={true} style={styles.segmentedControl}
// 可以自定义modal的一些属性 sliderStyle={styles.sliderStyle}
modalProps={{
}}
modalVisible={showDrawer}
// modalVisible回调函数
onModalVisibleChange={(visible) => {
}}
onPickerCancel={() => {
}}
onPickerConfirm={(date) => {
setShowDrawer(false)
}}
/> />
</>
{/* 日期选择器占位 */}
<View style={styles.datePickerPlaceholder}>
<Text style={{ color: '#888' }}>这里显示日期选择器</Text>
</View>
</View>
) )
} }
const styles = StyleSheet.create({
quickOption: {
paddingVertical: 6,
paddingHorizontal: 12,
backgroundColor: '#fff',
borderRadius: 6,
borderWidth: 1,
borderColor: '#ccc',
marginRight: 8,
height: 32,
},
quickOptionText: {
fontSize: 14,
color: '#333',
},
segmentedControl: {
borderRadius: 0,
height: 36,
backgroundColor: '#e5e7eb',
marginBottom: 12,
},
sliderStyle: {
borderRadius: 0,
backgroundColor: '#2196f3',
},
datePickerPlaceholder: {
height: 200,
backgroundColor: '#fff',
borderRadius: 6,
justifyContent: 'center',
alignItems: 'center',
},
})
@@ -1,5 +1,7 @@
import type { MoneyCategoryItem } from '@/types/money/category' import type { MoneyCategoryItem } from '@/types/money/category'
import { useCallback, useEffect, useMemo, useRef } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { AInput, LucideIcon } from '@/components/common/ao'
import EmptyState from '@/components/common/EmptyStatus'
import { import {
Actionsheet, Actionsheet,
ActionsheetBackdrop, ActionsheetBackdrop,
@@ -34,6 +36,7 @@ export default function CategorySheet({
}: CategorySheetProps) { }: CategorySheetProps) {
const listRef = useRef<any>(null) const listRef = useRef<any>(null)
const hasAutoScrolled = useRef(false) const hasAutoScrolled = useRef(false)
const [searchText, setSearchText] = useState('')
// 记住目标滚动位置 // 记住目标滚动位置
const pendingScrollRef = useRef<{ const pendingScrollRef = useRef<{
@@ -128,17 +131,45 @@ export default function CategorySheet({
} }
}, [open, value, sections]) }, [open, value, sections])
const filteredSections = useMemo<CategorySection[]>(() => {
if (!searchText.trim()) {
return sections
}
const lower = searchText.trim().toLowerCase()
return sections.map(section => ({
...section,
data: section.data.filter(item =>
item.name.toLowerCase().includes(lower),
),
})).filter(section => section.data.length > 0)
}, [sections, searchText])
function handleClose() {
onClose?.()
setSearchText('')
}
return ( return (
<Actionsheet isOpen={open} onClose={onClose}> <Actionsheet isOpen={open} onClose={handleClose}>
<ActionsheetBackdrop /> <ActionsheetBackdrop />
<ActionsheetContent style={{ maxHeight: '60%' }}> <ActionsheetContent style={{ maxHeight: '60%', minHeight: '60%' }}>
<ActionsheetDragIndicatorWrapper> <ActionsheetDragIndicatorWrapper>
<ActionsheetDragIndicator /> <ActionsheetDragIndicator />
</ActionsheetDragIndicatorWrapper> </ActionsheetDragIndicatorWrapper>
<AInput
placeholder="请输入搜索关键词"
className="mt-2"
value={searchText}
onChangeText={setSearchText}
leftIcon={() => <LucideIcon icon="Search" size={16} color="#666" />}
/>
<ActionsheetSectionList <ActionsheetSectionList
ref={listRef} ref={listRef}
sections={sections} sections={filteredSections}
stickySectionHeadersEnabled={true} stickySectionHeadersEnabled={true}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
onEndReachedThreshold={0.1} onEndReachedThreshold={0.1}
@@ -177,7 +208,13 @@ export default function CategorySheet({
const s = section as CategorySection const s = section as CategorySection
return ( return (
<ActionsheetSectionHeaderText className="font-semibold text-[black] text-sm bg-white"> <ActionsheetSectionHeaderText
className="font-semibold text-[black] text-sm bg-white"
style={{
paddingLeft: 0,
paddingRight: 0,
}}
>
{s.title} {s.title}
{' '} {' '}
( (
@@ -187,6 +224,10 @@ export default function CategorySheet({
) )
}} }}
/> />
{filteredSections.length === 0 && (
<EmptyState title="空" />
)}
</ActionsheetContent> </ActionsheetContent>
</Actionsheet> </Actionsheet>
) )
@@ -2,7 +2,6 @@ import type { AnalysisMoneyAmount } from '@/types/money/analysis'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { ScrollView, Text, TouchableOpacity, View } from 'react-native' import { ScrollView, Text, TouchableOpacity, View } from 'react-native'
import Svg from 'react-native-svg'
import { VictoryChart, VictoryLabel, VictoryLine, VictoryPie, VictoryTheme, VictoryTooltip } from 'victory-native' import { VictoryChart, VictoryLabel, VictoryLine, VictoryPie, VictoryTheme, VictoryTooltip } from 'victory-native'
import { ADatePicker, ADateRangePicker, ASegmentedTabs, LucideIcon } from '@/components/common/ao' import { ADatePicker, ADateRangePicker, ASegmentedTabs, LucideIcon } from '@/components/common/ao'
import EmptyState from '@/components/common/EmptyStatus' import EmptyState from '@/components/common/EmptyStatus'
@@ -10,6 +9,7 @@ import CustomHeader from '@/components/custom/Header'
import PagerTabs from '@/components/custom/PagerTabs' import PagerTabs from '@/components/custom/PagerTabs'
import { analysisMoneyLineApi, analysisMoneyPieApi } from '@/request/api/money/analysis' import { analysisMoneyLineApi, analysisMoneyPieApi } from '@/request/api/money/analysis'
import { MoneyCategoryEnum } from '@/types/money/category' import { MoneyCategoryEnum } from '@/types/money/category'
import { add } from '@/utils/math'
import { getCurrentYear, getCurrentYearMonth, getEndOfMonth, getStartOfMonth } from '@/utils/time' import { getCurrentYear, getCurrentYearMonth, getEndOfMonth, getStartOfMonth } from '@/utils/time'
interface AnalysisContentProps { interface AnalysisContentProps {
@@ -55,10 +55,6 @@ export default function Analysis() {
periodType = 1 periodType = 1
} }
else { else {
console.log(selectedStartDate.isSame(selectedEndDate, 'day'))
console.log(selectedStartDate.isSame(selectedEndDate, 'month'))
console.log(selectedStartDate.isSame(selectedEndDate, 'year'))
if (selectedStartDate.isSame(selectedEndDate, 'month')) { if (selectedStartDate.isSame(selectedEndDate, 'month')) {
periodType = 0 periodType = 0
start = selectedStartDate.startOf('month').format('YYYY-MM-DD') start = selectedStartDate.startOf('month').format('YYYY-MM-DD')
@@ -98,6 +94,10 @@ export default function Analysis() {
} }
}) })
} }
const totalAmount: number = pieRecords.reduce(
(sum, item) => Number(add(sum, Number(item.y) || 0)),
0,
)
useEffect(() => { useEffect(() => {
fetchChartsData() fetchChartsData()
@@ -190,16 +190,24 @@ export default function Analysis() {
</View> </View>
<View className="mt-4" style={{ paddingBottom: 24 }}> <View className="mt-4" style={{ paddingBottom: 24 }}>
<Text className="text-lg font-semibold text-gray-700">分类详细</Text> <View className="flex flex-row justify-between">
<Text className="text-lg font-semibold text-gray-700">分类详细</Text>
<Text>
总:
{totalAmount}
{' '}
元
</Text>
</View>
{ {
pieRecords.length > 0 pieRecords.length > 0
? ( ? (
<View className="mt-4 space-y-3"> <View className="mt-4 flex flex-col gap-2">
{pieRecords.map((item, index) => ( {pieRecords.map((item, index) => (
<View <View
key={index} key={index}
className={`flex flex-row justify-between items-center p-3 bg-white shadow-md rounded-lg ${index !== pieRecords.length - 1 ? 'border-b border-gray-300' : ''}`} className="flex flex-row justify-between items-center py-3 border-b border-gray-300"
> >
<Text className="text-base font-medium text-gray-800">{item.x}</Text> <Text className="text-base font-medium text-gray-800">{item.x}</Text>
<Text className="text-base font-semibold text-gray-700"> <Text className="text-base font-semibold text-gray-700">
@@ -0,0 +1,3 @@
export default function ADateMorePicker() {
}
@@ -1,9 +1,12 @@
import type { IDatePickerProps } from '@yz1311/react-native-wheel-picker' import type { IDatePickerProps } from '@yz1311/react-native-wheel-picker'
import SegmentedControl from '@react-native-segmented-control/segmented-control'
import { DatePicker } from '@yz1311/react-native-wheel-picker' import { DatePicker } from '@yz1311/react-native-wheel-picker'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { Animated, Modal, StyleSheet, TouchableOpacity } from 'react-native' import { Animated, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { AButton, ATab } from '@/components/common/ao'
import { toDate } from '@/utils/time' import { toDate } from '@/utils/time'
// 定义 mode 类型 // 定义 mode 类型
export type DatePickerMode = 'year' | 'month' | 'date' | 'time' | 'datetime' export type DatePickerMode = 'year' | 'month' | 'date' | 'time' | 'datetime'
@@ -32,13 +35,14 @@ export default function ADatePicker({
show = false, show = false,
onClose, onClose,
onConfirm, onConfirm,
mode = 'date',
format, format,
...restProps ...restProps
}: ADatePickerProps) { }: ADatePickerProps) {
const fadeAnim = useRef(new Animated.Value(0)).current const fadeAnim = useRef(new Animated.Value(0)).current
const slideAnim = useRef(new Animated.Value(300)).current const slideAnim = useRef(new Animated.Value(300)).current
const [visible, setVisible] = useState(show) const [visible, setVisible] = useState(show)
const [index, setIndex] = useState(0)
const [mode, setMode] = useState<DatePickerMode>('month')
// 使用传入 format 或默认 format // 使用传入 format 或默认 format
const finalFormat = format || defaultFormatMap[mode] const finalFormat = format || defaultFormatMap[mode]
@@ -80,6 +84,58 @@ export default function ADatePicker({
<Animated.View <Animated.View
style={[styles.pickerContainer, { transform: [{ translateY: slideAnim }] }]} style={[styles.pickerContainer, { transform: [{ translateY: slideAnim }] }]}
> >
<View className="px-3 pt-2 flex flex-col gap-2">
<View className="w-full">
<Text>快速选择</Text>
<ATab
data={[
{
label: '自定义',
key: '自定义',
},
{
label: '不限',
key: '不限',
},
{
label: '今天',
key: '今天',
},
{
label: '本月',
key: '本月',
},
{
label: '本年',
key: '本年',
},
{
label: '范围',
key: '范围',
},
]}
onChange={(key) => {
setMode('datetime')
}}
/>
</View>
<View className="w-full">
<Text>选择模式</Text>
<SegmentedControl
values={['日', '月', '年', '时间', '日期']}
selectedIndex={index}
style={{ borderRadius: 2, width: '100%' }}
sliderStyle={{
borderRadius: 2,
}}
onChange={(event: any) => setIndex(event.nativeEvent.selectedSegmentIndex)}
/>
</View>
</View>
<DatePicker <DatePicker
{...restProps} {...restProps}
date={toDate(date)} date={toDate(date)}
@@ -87,7 +143,16 @@ export default function ADatePicker({
isModal={false} isModal={false}
onPickerCancel={handleCancel} onPickerCancel={handleCancel}
onPickerConfirm={handleConfirm} onPickerConfirm={handleConfirm}
showHeader={false}
onDateChange={(value) => {
console.log(value)
}}
/> />
<View className="flex flex-row gap-4 px-3 pb-1 v">
<AButton variant="outline" className="flex-1">取消</AButton>
<AButton className="flex-1">确认</AButton>
</View>
</Animated.View> </Animated.View>
</Modal> </Modal>
) )
@@ -39,7 +39,7 @@ interface SegmentedTabsProps {
width?: number width?: number
} }
export default function SegmentedTabs({ export default function SegmentedTabs({
tabs, tabs,
value, value,
defaultIndex = 0, defaultIndex = 0,
@@ -1,5 +1,5 @@
import React, { useMemo, useRef, useState } from 'react' import React, { useMemo, useRef, useState } from 'react'
import { Pressable, Text, View } from 'react-native' import { Pressable, ScrollView, Text, View } from 'react-native'
import PagerView from 'react-native-pager-view' import PagerView from 'react-native-pager-view'
interface TabItem { interface TabItem {
@@ -12,18 +12,12 @@ interface TabProps {
data?: TabItem[] data?: TabItem[]
defaultKey?: TabItem['key'] defaultKey?: TabItem['key']
onChange?: (key: TabItem['key']) => void onChange?: (key: TabItem['key']) => void
tabBarClass?: string
tabItemClass?: string
activeTabItemClass?: string
} }
export default function Tab({ export default function Tab({
data, data,
defaultKey, defaultKey,
onChange, onChange,
tabBarClass = 'flex flex-row gap-2 overflow-hidden',
tabItemClass = 'items-center justify-center rounded-md px-4 py-2 bg-gray-200',
activeTabItemClass = 'items-center justify-center rounded-md px-4 py-2 bg-gray-800',
}: TabProps) { }: TabProps) {
if (!data || data.length === 0) if (!data || data.length === 0)
return null return null
@@ -72,22 +66,34 @@ export default function Tab({
return ( return (
<View className="flex-1"> <View className="flex-1">
{/* tab bar */} {/* tab bar */}
<View className={tabBarClass}> <ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
flexDirection: 'row',
gap: 8,
}}
>
{data.map((item) => { {data.map((item) => {
const isActive = item.key === selectedKey const isActive = item.key === selectedKey
return ( return (
<Pressable <Pressable
key={item.key} key={item.key}
onPress={() => handlePress(item.key)} onPress={() => handlePress(item.key)}
className={isActive ? activeTabItemClass : tabItemClass} className={
isActive
? 'items-center justify-center rounded-md px-4 py-2 bg-gray-800'
: 'items-center justify-center rounded-md px-4 py-2 bg-gray-200'
}
> >
<Text className={isActive ? 'text-white' : 'text-gray-700'}> <Text className={isActive ? 'text-white' : 'text-gray-700'}>
{item.label} {item.label}
</Text> </Text>
</Pressable> </Pressable>
) )
})} })}
</View> </ScrollView>
{hasContent && ( {hasContent && (
<PagerView <PagerView