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/motion": "2.4.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",
"@yz1311/react-native-wheel-picker": "^0.4.5",
"alova": "^3.3.4",
+14
View File
@@ -32,6 +32,9 @@ importers:
'@react-native-async-storage/async-storage':
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))
'@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':
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)
@@ -1702,6 +1705,12 @@ packages:
peerDependencies:
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':
resolution: {integrity: sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w==}
engines: {node: '>= 20.19.4'}
@@ -8278,6 +8287,11 @@ snapshots:
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-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/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 React from 'react'
import { Button, ButtonText } from '@/components/ui/button'
import SegmentedControl from '@react-native-segmented-control/segmented-control'
import React, { useState } from 'react'
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 (
<>
<Button
onPress={() => {
setShowDrawer(true)
}}
className="mt-10"
>
<ButtonText>Open Drawer</ButtonText>
</Button>
<View style={{ flex: 1, backgroundColor: '#f3f4f6', padding: 16 }}>
{/* 快捷选项栏 */}
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ marginBottom: 12 }}>
<ATab
data={[
{
label: '不限',
key: '不限',
},
{
label: '今天',
key: '今天',
},
{
label: '本月',
key: '本月',
},
{
label: '本年',
key: '本年',
},
]}
/>
</ScrollView>
<DateRangePicker
pickerTitle="预约时间"
pickerCancelBtnText="取消"
pickerConfirmBtnText="确认"
// 开启modal模式
isModal={true}
// 可以自定义modal的一些属性
modalProps={{
}}
modalVisible={showDrawer}
// modalVisible回调函数
onModalVisibleChange={(visible) => {
}}
onPickerCancel={() => {
}}
onPickerConfirm={(date) => {
setShowDrawer(false)
}}
{/* SegmentedControl */}
<SegmentedControl
values={['时间', '日', '月', '年', '范围']}
selectedIndex={index}
onChange={event => setIndex(event.nativeEvent.selectedSegmentIndex)}
style={styles.segmentedControl}
sliderStyle={styles.sliderStyle}
/>
</>
{/* 日期选择器占位 */}
<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 { 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 {
Actionsheet,
ActionsheetBackdrop,
@@ -34,6 +36,7 @@ export default function CategorySheet({
}: CategorySheetProps) {
const listRef = useRef<any>(null)
const hasAutoScrolled = useRef(false)
const [searchText, setSearchText] = useState('')
// 记住目标滚动位置
const pendingScrollRef = useRef<{
@@ -128,17 +131,45 @@ export default function CategorySheet({
}
}, [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 (
<Actionsheet isOpen={open} onClose={onClose}>
<Actionsheet isOpen={open} onClose={handleClose}>
<ActionsheetBackdrop />
<ActionsheetContent style={{ maxHeight: '60%' }}>
<ActionsheetContent style={{ maxHeight: '60%', minHeight: '60%' }}>
<ActionsheetDragIndicatorWrapper>
<ActionsheetDragIndicator />
</ActionsheetDragIndicatorWrapper>
<AInput
placeholder="请输入搜索关键词"
className="mt-2"
value={searchText}
onChangeText={setSearchText}
leftIcon={() => <LucideIcon icon="Search" size={16} color="#666" />}
/>
<ActionsheetSectionList
ref={listRef}
sections={sections}
sections={filteredSections}
stickySectionHeadersEnabled={true}
showsVerticalScrollIndicator={false}
onEndReachedThreshold={0.1}
@@ -177,7 +208,13 @@ export default function CategorySheet({
const s = section as CategorySection
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}
{' '}
(
@@ -187,6 +224,10 @@ export default function CategorySheet({
)
}}
/>
{filteredSections.length === 0 && (
<EmptyState title="空" />
)}
</ActionsheetContent>
</Actionsheet>
)
@@ -2,7 +2,6 @@ import type { AnalysisMoneyAmount } from '@/types/money/analysis'
import dayjs from 'dayjs'
import { useEffect, useState } from 'react'
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 { ADatePicker, ADateRangePicker, ASegmentedTabs, LucideIcon } from '@/components/common/ao'
import EmptyState from '@/components/common/EmptyStatus'
@@ -10,6 +9,7 @@ import CustomHeader from '@/components/custom/Header'
import PagerTabs from '@/components/custom/PagerTabs'
import { analysisMoneyLineApi, analysisMoneyPieApi } from '@/request/api/money/analysis'
import { MoneyCategoryEnum } from '@/types/money/category'
import { add } from '@/utils/math'
import { getCurrentYear, getCurrentYearMonth, getEndOfMonth, getStartOfMonth } from '@/utils/time'
interface AnalysisContentProps {
@@ -55,10 +55,6 @@ export default function Analysis() {
periodType = 1
}
else {
console.log(selectedStartDate.isSame(selectedEndDate, 'day'))
console.log(selectedStartDate.isSame(selectedEndDate, 'month'))
console.log(selectedStartDate.isSame(selectedEndDate, 'year'))
if (selectedStartDate.isSame(selectedEndDate, 'month')) {
periodType = 0
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(() => {
fetchChartsData()
@@ -190,16 +190,24 @@ export default function Analysis() {
</View>
<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
? (
<View className="mt-4 space-y-3">
<View className="mt-4 flex flex-col gap-2">
{pieRecords.map((item, index) => (
<View
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-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 SegmentedControl from '@react-native-segmented-control/segmented-control'
import { DatePicker } from '@yz1311/react-native-wheel-picker'
import dayjs from 'dayjs'
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'
// 定义 mode 类型
export type DatePickerMode = 'year' | 'month' | 'date' | 'time' | 'datetime'
@@ -32,13 +35,14 @@ export default function ADatePicker({
show = false,
onClose,
onConfirm,
mode = 'date',
format,
...restProps
}: ADatePickerProps) {
const fadeAnim = useRef(new Animated.Value(0)).current
const slideAnim = useRef(new Animated.Value(300)).current
const [visible, setVisible] = useState(show)
const [index, setIndex] = useState(0)
const [mode, setMode] = useState<DatePickerMode>('month')
// 使用传入 format 或默认 format
const finalFormat = format || defaultFormatMap[mode]
@@ -80,6 +84,58 @@ export default function ADatePicker({
<Animated.View
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
{...restProps}
date={toDate(date)}
@@ -87,7 +143,16 @@ export default function ADatePicker({
isModal={false}
onPickerCancel={handleCancel}
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>
</Modal>
)
@@ -39,7 +39,7 @@ interface SegmentedTabsProps {
width?: number
}
export default function SegmentedTabs({
export default function SegmentedTabs({
tabs,
value,
defaultIndex = 0,
@@ -1,5 +1,5 @@
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'
interface TabItem {
@@ -12,18 +12,12 @@ interface TabProps {
data?: TabItem[]
defaultKey?: TabItem['key']
onChange?: (key: TabItem['key']) => void
tabBarClass?: string
tabItemClass?: string
activeTabItemClass?: string
}
export default function Tab({
data,
defaultKey,
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) {
if (!data || data.length === 0)
return null
@@ -72,22 +66,34 @@ export default function Tab({
return (
<View className="flex-1">
{/* tab bar */}
<View className={tabBarClass}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
flexDirection: 'row',
gap: 8,
}}
>
{data.map((item) => {
const isActive = item.key === selectedKey
return (
<Pressable
key={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'}>
{item.label}
</Text>
</Pressable>
)
})}
</View>
</ScrollView>
{hasContent && (
<PagerView