u
This commit is contained in:
1 parent
bde3487da3
commit
7a9a22e02e
9 files changed
+243
-60
No files matched your search
@@ -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",
|
||||
|
||||
Generated
+14
@@ -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)':
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user