From ba9ce9e81b5db014d9406fdf7f98df7e42d0a24a Mon Sep 17 00:00:00 2001 From: oneao Date: Tue, 3 Feb 2026 22:19:45 +0800 Subject: [PATCH] u --- .../allapp-rn/src/app/(tabs)/_layout.tsx | 4 +- code/allapp/allapp-rn/src/app/(tabs)/love.tsx | 93 ++----- .../allapp-rn/src/app/(tabs)/money/index.tsx | 4 +- .../components/common/ao/DateMorePicker.tsx | 111 +++++++- .../src/components/common/ao/DatePicker.tsx | 9 +- .../src/components/common/ao/Drawer.tsx | 261 +++++++++++------- .../src/components/common/ao/Tab.tsx | 141 +++++----- .../src/components/common/ao/index.ts | 2 + 8 files changed, 377 insertions(+), 248 deletions(-) diff --git a/code/allapp/allapp-rn/src/app/(tabs)/_layout.tsx b/code/allapp/allapp-rn/src/app/(tabs)/_layout.tsx index 5d9315c7..63db88c6 100644 --- a/code/allapp/allapp-rn/src/app/(tabs)/_layout.tsx +++ b/code/allapp/allapp-rn/src/app/(tabs)/_layout.tsx @@ -86,7 +86,9 @@ export default function TabsLayout() { zIndex: 0, elevation: 24, shadowColor: '#000', - shadowOffset: { width: 0, height: 100 }, + shadowOffset: { width: 0, height: 20 }, + borderTopWidth: 1, + borderTopColor: '#f2f3f5', }} > diff --git a/code/allapp/allapp-rn/src/app/(tabs)/love.tsx b/code/allapp/allapp-rn/src/app/(tabs)/love.tsx index def56adc..ddca6d11 100644 --- a/code/allapp/allapp-rn/src/app/(tabs)/love.tsx +++ b/code/allapp/allapp-rn/src/app/(tabs)/love.tsx @@ -1,85 +1,28 @@ 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' +import { AButton, ADateMorePicker, ATab } from '@/components/common/ao' export default function DemoDatePickerUI() { - const [index, setIndex] = useState(0) + const [show, setShow] = useState(false) return ( - - {/* 快捷选项栏 */} - - - - - {/* SegmentedControl */} - setIndex(event.nativeEvent.selectedSegmentIndex)} - style={styles.segmentedControl} - sliderStyle={styles.sliderStyle} - /> - - {/* 日期选择器占位 */} - - 这里显示日期选择器 - + + { + setShow(true) + }} + className="mt-20" + > + 测试 + + { + setShow(false) + }} + > + ) } - -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', - }, -}) diff --git a/code/allapp/allapp-rn/src/app/(tabs)/money/index.tsx b/code/allapp/allapp-rn/src/app/(tabs)/money/index.tsx index ee40ff79..e3cb44ca 100644 --- a/code/allapp/allapp-rn/src/app/(tabs)/money/index.tsx +++ b/code/allapp/allapp-rn/src/app/(tabs)/money/index.tsx @@ -209,7 +209,7 @@ export default function Money() { category: category.id, })) }} - /> + /> diff --git a/code/allapp/allapp-rn/src/components/common/ao/DateMorePicker.tsx b/code/allapp/allapp-rn/src/components/common/ao/DateMorePicker.tsx index 8cb4ff42..0ca737a6 100644 --- a/code/allapp/allapp-rn/src/components/common/ao/DateMorePicker.tsx +++ b/code/allapp/allapp-rn/src/components/common/ao/DateMorePicker.tsx @@ -1,3 +1,112 @@ -export default function ADateMorePicker() { +import SegmentedControl from '@react-native-segmented-control/segmented-control' +import { DateRangePicker } from '@yz1311/react-native-wheel-picker' +import { useEffect, useState } from 'react' +import { Text, View } from 'react-native' +import Button from './Button' +import Drawer from './Drawer' +import Tab from './Tab' +export type DatePickerMode = 'year' | 'month' | 'date' | 'time' | 'datetime' +export type DateFormatType + = | 'YYYY' + | 'YYYY-MM' + | 'YYYY-MM-DD' + | 'HH:mm:ss' + | 'YYYY-MM-DD HH:mm:ss' + +interface DateMorePickerProps { + startDate?: Date | string + endDate?: Date | string + visible?: boolean + onClose?: () => void + onConfirm?: (value: string) => void + mode?: DatePickerMode + format?: DateFormatType +} + +export default function DateMorePicker({ + visible, + onClose, +}: DateMorePickerProps) { + const [localVisible, setLocalVisible] = useState(visible || false) + const [index, setIndex] = useState(0) + + useEffect(() => { + setLocalVisible(!!visible) + }, [visible]) + + function handleClose() { + setLocalVisible(false) + onClose?.() + } + + return ( + + {/* 外层必须 flex:1 */} + + {/* ===== 顶部固定区域 ===== */} + + + 快速选择 + {}} + /> + + + + 选择模式 + + setIndex(e.nativeEvent.selectedSegmentIndex)} + /> + + + + {/* ===== 中间:吃掉剩余高度 ===== */} + + + + + {/* ===== 底部固定按钮 ===== */} + + + + + + + ) } diff --git a/code/allapp/allapp-rn/src/components/common/ao/DatePicker.tsx b/code/allapp/allapp-rn/src/components/common/ao/DatePicker.tsx index a5e9f17f..3c46cf5b 100644 --- a/code/allapp/allapp-rn/src/components/common/ao/DatePicker.tsx +++ b/code/allapp/allapp-rn/src/components/common/ao/DatePicker.tsx @@ -4,7 +4,6 @@ import { DatePicker } from '@yz1311/react-native-wheel-picker' import dayjs from 'dayjs' import React, { useEffect, useRef, useState } from 'react' import { Animated, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native' -import { AButton, ATab } from '@/components/common/ao' import { toDate } from '@/utils/time' // 定义 mode 类型 @@ -87,7 +86,7 @@ export default function ADatePicker({ 快速选择 - { setMode('datetime') }} - /> + /> */} @@ -150,8 +149,8 @@ export default function ADatePicker({ /> - 取消 - 确认 + {/* 取消 + 确认 */} diff --git a/code/allapp/allapp-rn/src/components/common/ao/Drawer.tsx b/code/allapp/allapp-rn/src/components/common/ao/Drawer.tsx index 557d839e..9fbb30a8 100644 --- a/code/allapp/allapp-rn/src/components/common/ao/Drawer.tsx +++ b/code/allapp/allapp-rn/src/components/common/ao/Drawer.tsx @@ -1,14 +1,16 @@ import type { ReactNode } from 'react' -import type { DimensionValue, ViewStyle } from 'react-native' -import React, { useEffect, useMemo, useState } from 'react' -import { Dimensions, StyleSheet, Text, View } from 'react-native' -import Animated, { - interpolate, - runOnJS, - useAnimatedStyle, - useSharedValue, - withTiming, -} from 'react-native-reanimated' +import type { ViewStyle } from 'react-native' +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { + Animated, + Dimensions, + Modal, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native' + import LucideIcon from './LucideIcon' const SCREEN = Dimensions.get('screen') @@ -17,13 +19,13 @@ interface DrawerMenuProps { visible: boolean duration?: number menuPosition?: 'left' | 'right' | 'top' | 'bottom' - height?: DimensionValue + height?: number | string + width?: number | string hideModal: () => void children?: ReactNode header?: ReactNode isCircle?: boolean CircleNum?: number - isShadowHideModal?: boolean title?: string showHeader?: boolean } @@ -31,134 +33,189 @@ interface DrawerMenuProps { export default function Drawer({ visible, height = 300, + width = 300, hideModal, duration = 300, isCircle, menuPosition = 'bottom', children, CircleNum = 12, - isShadowHideModal = true, header, title, showHeader = true, }: DrawerMenuProps) { - const progress = useSharedValue(0) - const drag = useSharedValue(0) + const fadeAnim = useRef(new Animated.Value(0)).current + const slideAnim = useRef(new Animated.Value(0)).current + const [innerVisible, setInnerVisible] = useState(false) - /* ================= 显隐 ================= */ - useEffect(() => { - if (visible) { - setInnerVisible(true) - progress.value = withTiming(1, { duration }) + /* ================= 尺寸计算 ================= */ + const drawerHeight = useMemo(() => { + if (typeof height === 'string' && height.endsWith('%')) { + return (Number.parseFloat(height) / 100) * SCREEN.height } - else { - progress.value = withTiming(0, { duration }, () => { - runOnJS(setInnerVisible)(false) - }) + return Number(height) + }, [height]) + + const drawerWidth = useMemo(() => { + if (typeof width === 'string' && width.endsWith('%')) { + return (Number.parseFloat(width) / 100) * SCREEN.width } - }, [visible]) + return Number(width) + }, [width]) /* ================= Drawer 尺寸 ================= */ const drawerBox = useMemo(() => { switch (menuPosition) { case 'left': - return { width: height, left: 0, top: 0, bottom: 0 } + return { width: drawerWidth, left: 0, top: 0, bottom: 0 } case 'right': - return { width: height, right: 0, top: 0, bottom: 0 } + return { width: drawerWidth, right: 0, top: 0, bottom: 0 } case 'top': - return { height, left: 0, right: 0, top: 0 } + return { height: drawerHeight, left: 0, right: 0, top: 0 } case 'bottom': default: - return { height, left: 0, right: 0, bottom: 0 } + return { height: drawerHeight, left: 0, right: 0, bottom: 0 } } - }, [menuPosition, height]) + }, [menuPosition, drawerHeight, drawerWidth]) - /* ================= 动画样式(关键) ================= */ - const drawerStyle = useAnimatedStyle(() => { + /* ================= 动画方向 ================= */ + const transformStyle = useMemo(() => { switch (menuPosition) { - case 'bottom': { - const y = interpolate(progress.value, [0, 1], [SCREEN.height, 0], 'clamp') - return { transform: [{ translateY: y + drag.value }] } - } - case 'top': { - const y = interpolate(progress.value, [0, 1], [-SCREEN.height, 0], 'clamp') - return { transform: [{ translateY: y }] } - } - case 'left': { - const x = interpolate(progress.value, [0, 1], [-SCREEN.width, 0], 'clamp') - return { transform: [{ translateX: x }] } - } - case 'right': { - const x = interpolate(progress.value, [0, 1], [SCREEN.width, 0], 'clamp') - return { transform: [{ translateX: x }] } - } + case 'left': + case 'right': + return { transform: [{ translateX: slideAnim }] } + default: + return { transform: [{ translateY: slideAnim }] } + } + }, [menuPosition, slideAnim]) + + /* ================= 显隐动画(关键修改点) ================= */ + useEffect(() => { + let initialValue = 0 + + switch (menuPosition) { + case 'bottom': + initialValue = drawerHeight + break + case 'top': + initialValue = -drawerHeight + break + case 'left': + initialValue = -drawerWidth + break + case 'right': + initialValue = drawerWidth + break + } + + if (visible) { + setInnerVisible(true) + + // ⚠️ 关键:等 Animated.View mount 之后,再初始化位置 + requestAnimationFrame(() => { + fadeAnim.setValue(0) + slideAnim.setValue(initialValue) + + Animated.parallel([ + Animated.timing(fadeAnim, { + toValue: 1, + duration, + useNativeDriver: true, + }), + Animated.timing(slideAnim, { + toValue: 0, + duration, + useNativeDriver: true, + }), + ]).start() + }) + } + else { + Animated.parallel([ + Animated.timing(fadeAnim, { + toValue: 0, + duration, + useNativeDriver: true, + }), + Animated.timing(slideAnim, { + toValue: initialValue, + duration, + useNativeDriver: true, + }), + ]).start(() => { + setInnerVisible(false) + }) + } + }, [visible, drawerHeight, drawerWidth, menuPosition, duration]) + + /* ================= 圆角 ================= */ + const borderRadiusStyle = useMemo(() => { + if (!isCircle) + return {} + switch (menuPosition) { + case 'bottom': + return { borderTopLeftRadius: CircleNum, borderTopRightRadius: CircleNum } + case 'top': + return { borderBottomLeftRadius: CircleNum, borderBottomRightRadius: CircleNum } + case 'left': + return { borderTopRightRadius: CircleNum, borderBottomRightRadius: CircleNum } + case 'right': + return { borderTopLeftRadius: CircleNum, borderBottomLeftRadius: CircleNum } default: return {} } - }) - - const maskStyle = useAnimatedStyle(() => ({ - opacity: interpolate(progress.value, [0, 1], [0, 0.45], 'clamp'), - })) - - - if (!innerVisible) - return null + }, [menuPosition, isCircle, CircleNum]) return ( - - {/* 遮罩 */} - isShadowHideModal && hideModal()} - /> - - {/* Drawer */} - - - {showHeader && ( - header ?? ( - - {title} - - - ) - )} - - {children} + + + {/* 遮罩 */} + + + + {/* Drawer */} + + {innerVisible && ( + + {showHeader && ( + header ?? ( + + {title} + + + ) + )} + + {children} + + )} + - + ) } const styles = StyleSheet.create({ - mask: { + overlay: { ...StyleSheet.absoluteFillObject, - backgroundColor: '#000', + backgroundColor: 'rgba(0,0,0,0.5)', }, drawerBox: { position: 'absolute', }, - drawer: { - flex: 1, - backgroundColor: '#fff', - }, headTitle: { padding: 12, alignItems: 'center', @@ -173,4 +230,12 @@ const styles = StyleSheet.create({ right: 16, top: 12, }, + pickerContainer: { + backgroundColor: '#fff', + overflow: 'hidden', + position: 'absolute', + bottom: 0, + width: '100%', + height: '100%', + }, }) diff --git a/code/allapp/allapp-rn/src/components/common/ao/Tab.tsx b/code/allapp/allapp-rn/src/components/common/ao/Tab.tsx index 3b4b6474..7fa3f911 100644 --- a/code/allapp/allapp-rn/src/components/common/ao/Tab.tsx +++ b/code/allapp/allapp-rn/src/components/common/ao/Tab.tsx @@ -1,6 +1,15 @@ -import React, { useMemo, useRef, useState } from 'react' -import { Pressable, ScrollView, Text, View } from 'react-native' -import PagerView from 'react-native-pager-view' +import type { + LayoutChangeEvent, +} from 'react-native' +import React, { useEffect, useRef, useState } from 'react' +import { + NativeScrollEvent, + NativeSyntheticEvent, + ScrollView, + Text, + TouchableWithoutFeedback, + View, +} from 'react-native' interface TabItem { label: string @@ -22,93 +31,93 @@ export default function Tab({ if (!data || data.length === 0) return null - // 默认选中 key(没传就用第一个) + /* ================= state ================= */ const initialKey = defaultKey ?? data[0].key const [selectedKey, setSelectedKey] = useState(initialKey) - const pagerRef = useRef(null) + /* ================= refs ================= */ + const scrollRef = useRef(null) - // 是否有 content - const hasContent = useMemo( - () => data.some(item => item.content), - [data], - ) + // 每个 tab 的 layout 信息 + const itemLayouts = useRef< + Record< + TabItem['key'], + { x: number, width: number } + > + >({}) - // key -> index - const getIndexByKey = (key: TabItem['key']) => - data.findIndex(item => item.key === key) - - // index -> key - const getKeyByIndex = (index: number) => - data[index]?.key + const containerWidth = useRef(0) + /* ================= handlers ================= */ const handlePress = (key: TabItem['key']) => { setSelectedKey(key) onChange?.(key) - - if (hasContent) { - const index = getIndexByKey(key) - if (index !== -1) - pagerRef.current?.setPage(index) - } + scrollToCenter(key) } - const onPageSelected = (e: any) => { - const index = e.nativeEvent.position - const key = getKeyByIndex(index) + const scrollToCenter = (key: TabItem['key']) => { + const layout = itemLayouts.current[key] + if (!layout || !scrollRef.current) + return - if (key !== undefined) { - setSelectedKey(key) - onChange?.(key) - } + const offset + = layout.x + layout.width / 2 - containerWidth.current / 2 + + scrollRef.current.scrollTo({ + x: Math.max(0, offset), + animated: true, + }) } + /* ================= effects ================= */ + useEffect(() => { + // 首次挂载后,让默认选中项居中 + requestAnimationFrame(() => { + scrollToCenter(selectedKey) + }) + }, []) + + /* ================= render ================= */ return ( - - {/* tab bar */} - - {data.map((item) => { - const isActive = item.key === selectedKey - return ( - handlePress(item.key)} + { + containerWidth.current = e.nativeEvent.layout.width + }} + contentContainerStyle={{ + flexDirection: 'row', + gap: 8, + }} + > + {data.map((item) => { + const isActive = item.key === selectedKey + + return ( + handlePress(item.key)} + > + { + const { x, width } = e.nativeEvent.layout + itemLayouts.current[item.key] = { x, width } + }} 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' } > - {item.label} - - ) - })} - - - {hasContent && ( - - {data.map(item => ( - - {item.content} - ))} - - )} - + + ) + })} + ) } diff --git a/code/allapp/allapp-rn/src/components/common/ao/index.ts b/code/allapp/allapp-rn/src/components/common/ao/index.ts index 414d2e6e..61f09116 100644 --- a/code/allapp/allapp-rn/src/components/common/ao/index.ts +++ b/code/allapp/allapp-rn/src/components/common/ao/index.ts @@ -3,6 +3,7 @@ import Button from './Button' import { Cell, CellGroup } from './Cell' import Checkbox from './Checkbox' +import DateMorePicker from './DateMorePicker' import DatePicker from './DatePicker' import DateRangePicker from './DateRangePicker' import Divider from './Divider' @@ -22,6 +23,7 @@ export { Cell as ACell, CellGroup as ACellGroup, Checkbox as ACheckbox, + DateMorePicker as ADateMorePicker, DatePicker as ADatePicker, DateRangePicker as ADateRangePicker, Divider as ADivider,