u
This commit is contained in:
1 parent
7a9a22e02e
commit
ba9ce9e81b
8 files changed
+377
-248
No files matched your search
@@ -86,7 +86,9 @@ export default function TabsLayout() {
|
|||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
elevation: 24,
|
elevation: 24,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 100 },
|
shadowOffset: { width: 0, height: 20 },
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#f2f3f5',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TouchableOpacity className="w-12 items-center justify-center">
|
<TouchableOpacity className="w-12 items-center justify-center">
|
||||||
|
|||||||
@@ -1,85 +1,28 @@
|
|||||||
import SegmentedControl from '@react-native-segmented-control/segmented-control'
|
import SegmentedControl from '@react-native-segmented-control/segmented-control'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
|
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() {
|
export default function DemoDatePickerUI() {
|
||||||
const [index, setIndex] = useState(0)
|
const [show, setShow] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: '#f3f4f6', padding: 16 }}>
|
<View className="flex-1">
|
||||||
{/* 快捷选项栏 */}
|
<AButton
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ marginBottom: 12 }}>
|
onPress={() => {
|
||||||
<ATab
|
setShow(true)
|
||||||
data={[
|
}}
|
||||||
{
|
className="mt-20"
|
||||||
label: '不限',
|
>
|
||||||
key: '不限',
|
测试
|
||||||
},
|
</AButton>
|
||||||
{
|
<ADateMorePicker
|
||||||
label: '今天',
|
visible={show}
|
||||||
key: '今天',
|
onClose={() => {
|
||||||
},
|
setShow(false)
|
||||||
{
|
}}
|
||||||
label: '本月',
|
>
|
||||||
key: '本月',
|
</ADateMorePicker>
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '本年',
|
|
||||||
key: '本年',
|
|
||||||
},
|
|
||||||
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
{/* 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>
|
</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',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -209,7 +209,7 @@ export default function Money() {
|
|||||||
category: category.id,
|
category: category.id,
|
||||||
}))
|
}))
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ADrawer
|
<ADrawer
|
||||||
visible={showActionsMenu}
|
visible={showActionsMenu}
|
||||||
@@ -217,7 +217,7 @@ export default function Money() {
|
|||||||
setShowActionsMenu(false)
|
setShowActionsMenu(false)
|
||||||
}}
|
}}
|
||||||
menuPosition="right"
|
menuPosition="right"
|
||||||
height="60%"
|
width="60%"
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
>
|
>
|
||||||
<SafeAreaView className="flex-col px-3">
|
<SafeAreaView className="flex-col px-3">
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Drawer
|
||||||
|
visible={localVisible}
|
||||||
|
hideModal={handleClose}
|
||||||
|
showHeader={false}
|
||||||
|
height="60%"
|
||||||
|
>
|
||||||
|
{/* 外层必须 flex:1 */}
|
||||||
|
<View className="flex-1 p-3">
|
||||||
|
{/* ===== 顶部固定区域 ===== */}
|
||||||
|
<View
|
||||||
|
className="gap-2"
|
||||||
|
style={{ flexShrink: 0, height: 200 }}
|
||||||
|
>
|
||||||
|
<View>
|
||||||
|
<Text className="text-lg mb-1">快速选择</Text>
|
||||||
|
<Tab
|
||||||
|
data={[
|
||||||
|
{ label: '自定义', key: '自定义' },
|
||||||
|
{ label: '不限', key: '不限' },
|
||||||
|
{ label: '今天', key: '今天' },
|
||||||
|
{ label: '本月', key: '本月' },
|
||||||
|
{ label: '本年', key: '本年' },
|
||||||
|
{ label: '范围', key: '范围' },
|
||||||
|
]}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<Text className="text-lg mb-1">选择模式</Text>
|
||||||
|
<SegmentedControl
|
||||||
|
values={['日', '月', '年', '时间', '日期']}
|
||||||
|
selectedIndex={index}
|
||||||
|
style={{ width: '100%', borderRadius: 2 }}
|
||||||
|
sliderStyle={{ borderRadius: 2 }}
|
||||||
|
onChange={(e: any) =>
|
||||||
|
setIndex(e.nativeEvent.selectedSegmentIndex)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* ===== 中间:吃掉剩余高度 ===== */}
|
||||||
|
<View style={{ flex: 1, justifyContent: 'center' }}>
|
||||||
|
<DateRangePicker
|
||||||
|
isModal={false}
|
||||||
|
showHeader={false}
|
||||||
|
pickerWrapperStyle={{
|
||||||
|
height: 24,
|
||||||
|
minHeight: 24,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* ===== 底部固定按钮 ===== */}
|
||||||
|
<View
|
||||||
|
className="flex-row gap-3 pt-2"
|
||||||
|
style={{ flexShrink: 0, height: 100 }}
|
||||||
|
>
|
||||||
|
<Button className="flex-1" variant="outline" onPress={handleClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button className="flex-1">
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@ 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, Text, TouchableOpacity, View } 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 类型
|
||||||
@@ -87,7 +86,7 @@ export default function ADatePicker({
|
|||||||
<View className="px-3 pt-2 flex flex-col gap-2">
|
<View className="px-3 pt-2 flex flex-col gap-2">
|
||||||
<View className="w-full">
|
<View className="w-full">
|
||||||
<Text>快速选择</Text>
|
<Text>快速选择</Text>
|
||||||
<ATab
|
{/* <ATab
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
label: '自定义',
|
label: '自定义',
|
||||||
@@ -117,7 +116,7 @@ export default function ADatePicker({
|
|||||||
onChange={(key) => {
|
onChange={(key) => {
|
||||||
setMode('datetime')
|
setMode('datetime')
|
||||||
}}
|
}}
|
||||||
/>
|
/> */}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="w-full">
|
<View className="w-full">
|
||||||
@@ -150,8 +149,8 @@ export default function ADatePicker({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<View className="flex flex-row gap-4 px-3 pb-1 v">
|
<View className="flex flex-row gap-4 px-3 pb-1 v">
|
||||||
<AButton variant="outline" className="flex-1">取消</AButton>
|
{/* <AButton variant="outline" className="flex-1">取消</AButton>
|
||||||
<AButton className="flex-1">确认</AButton>
|
<AButton className="flex-1">确认</AButton> */}
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import type { DimensionValue, ViewStyle } from 'react-native'
|
import type { ViewStyle } from 'react-native'
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Dimensions, StyleSheet, Text, View } from 'react-native'
|
import {
|
||||||
import Animated, {
|
Animated,
|
||||||
interpolate,
|
Dimensions,
|
||||||
runOnJS,
|
Modal,
|
||||||
useAnimatedStyle,
|
StyleSheet,
|
||||||
useSharedValue,
|
Text,
|
||||||
withTiming,
|
TouchableOpacity,
|
||||||
} from 'react-native-reanimated'
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
|
||||||
import LucideIcon from './LucideIcon'
|
import LucideIcon from './LucideIcon'
|
||||||
|
|
||||||
const SCREEN = Dimensions.get('screen')
|
const SCREEN = Dimensions.get('screen')
|
||||||
@@ -17,13 +19,13 @@ interface DrawerMenuProps {
|
|||||||
visible: boolean
|
visible: boolean
|
||||||
duration?: number
|
duration?: number
|
||||||
menuPosition?: 'left' | 'right' | 'top' | 'bottom'
|
menuPosition?: 'left' | 'right' | 'top' | 'bottom'
|
||||||
height?: DimensionValue
|
height?: number | string
|
||||||
|
width?: number | string
|
||||||
hideModal: () => void
|
hideModal: () => void
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
header?: ReactNode
|
header?: ReactNode
|
||||||
isCircle?: boolean
|
isCircle?: boolean
|
||||||
CircleNum?: number
|
CircleNum?: number
|
||||||
isShadowHideModal?: boolean
|
|
||||||
title?: string
|
title?: string
|
||||||
showHeader?: boolean
|
showHeader?: boolean
|
||||||
}
|
}
|
||||||
@@ -31,134 +33,189 @@ interface DrawerMenuProps {
|
|||||||
export default function Drawer({
|
export default function Drawer({
|
||||||
visible,
|
visible,
|
||||||
height = 300,
|
height = 300,
|
||||||
|
width = 300,
|
||||||
hideModal,
|
hideModal,
|
||||||
duration = 300,
|
duration = 300,
|
||||||
isCircle,
|
isCircle,
|
||||||
menuPosition = 'bottom',
|
menuPosition = 'bottom',
|
||||||
children,
|
children,
|
||||||
CircleNum = 12,
|
CircleNum = 12,
|
||||||
isShadowHideModal = true,
|
|
||||||
header,
|
header,
|
||||||
title,
|
title,
|
||||||
showHeader = true,
|
showHeader = true,
|
||||||
}: DrawerMenuProps) {
|
}: DrawerMenuProps) {
|
||||||
const progress = useSharedValue(0)
|
const fadeAnim = useRef(new Animated.Value(0)).current
|
||||||
const drag = useSharedValue(0)
|
const slideAnim = useRef(new Animated.Value(0)).current
|
||||||
|
|
||||||
const [innerVisible, setInnerVisible] = useState(false)
|
const [innerVisible, setInnerVisible] = useState(false)
|
||||||
|
|
||||||
/* ================= 显隐 ================= */
|
/* ================= 尺寸计算 ================= */
|
||||||
useEffect(() => {
|
const drawerHeight = useMemo(() => {
|
||||||
if (visible) {
|
if (typeof height === 'string' && height.endsWith('%')) {
|
||||||
setInnerVisible(true)
|
return (Number.parseFloat(height) / 100) * SCREEN.height
|
||||||
progress.value = withTiming(1, { duration })
|
|
||||||
}
|
}
|
||||||
else {
|
return Number(height)
|
||||||
progress.value = withTiming(0, { duration }, () => {
|
}, [height])
|
||||||
runOnJS(setInnerVisible)(false)
|
|
||||||
})
|
const drawerWidth = useMemo(() => {
|
||||||
|
if (typeof width === 'string' && width.endsWith('%')) {
|
||||||
|
return (Number.parseFloat(width) / 100) * SCREEN.width
|
||||||
}
|
}
|
||||||
}, [visible])
|
return Number(width)
|
||||||
|
}, [width])
|
||||||
|
|
||||||
/* ================= Drawer 尺寸 ================= */
|
/* ================= Drawer 尺寸 ================= */
|
||||||
const drawerBox = useMemo<ViewStyle>(() => {
|
const drawerBox = useMemo<ViewStyle>(() => {
|
||||||
switch (menuPosition) {
|
switch (menuPosition) {
|
||||||
case 'left':
|
case 'left':
|
||||||
return { width: height, left: 0, top: 0, bottom: 0 }
|
return { width: drawerWidth, left: 0, top: 0, bottom: 0 }
|
||||||
case 'right':
|
case 'right':
|
||||||
return { width: height, right: 0, top: 0, bottom: 0 }
|
return { width: drawerWidth, right: 0, top: 0, bottom: 0 }
|
||||||
case 'top':
|
case 'top':
|
||||||
return { height, left: 0, right: 0, top: 0 }
|
return { height: drawerHeight, left: 0, right: 0, top: 0 }
|
||||||
case 'bottom':
|
case 'bottom':
|
||||||
default:
|
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) {
|
switch (menuPosition) {
|
||||||
case 'bottom': {
|
case 'left':
|
||||||
const y = interpolate(progress.value, [0, 1], [SCREEN.height, 0], 'clamp')
|
case 'right':
|
||||||
return { transform: [{ translateY: y + drag.value }] }
|
return { transform: [{ translateX: slideAnim }] }
|
||||||
}
|
default:
|
||||||
case 'top': {
|
return { transform: [{ translateY: slideAnim }] }
|
||||||
const y = interpolate(progress.value, [0, 1], [-SCREEN.height, 0], 'clamp')
|
}
|
||||||
return { transform: [{ translateY: y }] }
|
}, [menuPosition, slideAnim])
|
||||||
}
|
|
||||||
case 'left': {
|
/* ================= 显隐动画(关键修改点) ================= */
|
||||||
const x = interpolate(progress.value, [0, 1], [-SCREEN.width, 0], 'clamp')
|
useEffect(() => {
|
||||||
return { transform: [{ translateX: x }] }
|
let initialValue = 0
|
||||||
}
|
|
||||||
case 'right': {
|
switch (menuPosition) {
|
||||||
const x = interpolate(progress.value, [0, 1], [SCREEN.width, 0], 'clamp')
|
case 'bottom':
|
||||||
return { transform: [{ translateX: x }] }
|
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:
|
default:
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
})
|
}, [menuPosition, isCircle, CircleNum])
|
||||||
|
|
||||||
const maskStyle = useAnimatedStyle(() => ({
|
|
||||||
opacity: interpolate(progress.value, [0, 1], [0, 0.45], 'clamp'),
|
|
||||||
}))
|
|
||||||
|
|
||||||
|
|
||||||
if (!innerVisible)
|
|
||||||
return null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={StyleSheet.absoluteFill} pointerEvents="box-none" className="z-[99]">
|
<Modal
|
||||||
{/* 遮罩 */}
|
visible={innerVisible}
|
||||||
<Animated.View
|
transparent
|
||||||
style={[styles.mask, maskStyle]}
|
animationType="none"
|
||||||
onTouchEnd={() => isShadowHideModal && hideModal()}
|
onRequestClose={hideModal}
|
||||||
/>
|
>
|
||||||
|
<View style={StyleSheet.absoluteFill} pointerEvents="box-none">
|
||||||
{/* Drawer */}
|
{/* 遮罩 */}
|
||||||
<View style={[styles.drawerBox, drawerBox]}>
|
<Animated.View style={[styles.overlay, { opacity: fadeAnim }]}>
|
||||||
<Animated.View
|
<TouchableOpacity style={{ flex: 1 }} activeOpacity={1} onPress={hideModal} />
|
||||||
style={[
|
|
||||||
styles.drawer,
|
|
||||||
drawerStyle,
|
|
||||||
{
|
|
||||||
borderTopLeftRadius: isCircle ? CircleNum : 0,
|
|
||||||
borderTopRightRadius: isCircle ? CircleNum : 0,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{showHeader && (
|
|
||||||
header ?? (
|
|
||||||
<View style={styles.headTitle}>
|
|
||||||
<Text style={styles.title}>{title}</Text>
|
|
||||||
<LucideIcon
|
|
||||||
icon="X"
|
|
||||||
size={22}
|
|
||||||
style={styles.close}
|
|
||||||
onPress={hideModal}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Drawer */}
|
||||||
|
<View style={[styles.drawerBox, drawerBox]}>
|
||||||
|
{innerVisible && (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.pickerContainer,
|
||||||
|
transformStyle,
|
||||||
|
borderRadiusStyle,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{showHeader && (
|
||||||
|
header ?? (
|
||||||
|
<View style={styles.headTitle}>
|
||||||
|
<Text style={styles.title}>{title}</Text>
|
||||||
|
<LucideIcon icon="X" size={22} style={styles.close} onPress={hideModal} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
mask: {
|
overlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
backgroundColor: '#000',
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
},
|
},
|
||||||
drawerBox: {
|
drawerBox: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
},
|
},
|
||||||
drawer: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
},
|
|
||||||
headTitle: {
|
headTitle: {
|
||||||
padding: 12,
|
padding: 12,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -173,4 +230,12 @@ const styles = StyleSheet.create({
|
|||||||
right: 16,
|
right: 16,
|
||||||
top: 12,
|
top: 12,
|
||||||
},
|
},
|
||||||
|
pickerContainer: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
@@ -1,6 +1,15 @@
|
|||||||
import React, { useMemo, useRef, useState } from 'react'
|
import type {
|
||||||
import { Pressable, ScrollView, Text, View } from 'react-native'
|
LayoutChangeEvent,
|
||||||
import PagerView from 'react-native-pager-view'
|
} from 'react-native'
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
import {
|
||||||
|
NativeScrollEvent,
|
||||||
|
NativeSyntheticEvent,
|
||||||
|
ScrollView,
|
||||||
|
Text,
|
||||||
|
TouchableWithoutFeedback,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
|
||||||
interface TabItem {
|
interface TabItem {
|
||||||
label: string
|
label: string
|
||||||
@@ -22,93 +31,93 @@ export default function Tab({
|
|||||||
if (!data || data.length === 0)
|
if (!data || data.length === 0)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
// 默认选中 key(没传就用第一个)
|
/* ================= state ================= */
|
||||||
const initialKey = defaultKey ?? data[0].key
|
const initialKey = defaultKey ?? data[0].key
|
||||||
const [selectedKey, setSelectedKey] = useState(initialKey)
|
const [selectedKey, setSelectedKey] = useState(initialKey)
|
||||||
|
|
||||||
const pagerRef = useRef<PagerView>(null)
|
/* ================= refs ================= */
|
||||||
|
const scrollRef = useRef<ScrollView>(null)
|
||||||
|
|
||||||
// 是否有 content
|
// 每个 tab 的 layout 信息
|
||||||
const hasContent = useMemo(
|
const itemLayouts = useRef<
|
||||||
() => data.some(item => item.content),
|
Record<
|
||||||
[data],
|
TabItem['key'],
|
||||||
)
|
{ x: number, width: number }
|
||||||
|
>
|
||||||
|
>({})
|
||||||
|
|
||||||
// key -> index
|
const containerWidth = useRef(0)
|
||||||
const getIndexByKey = (key: TabItem['key']) =>
|
|
||||||
data.findIndex(item => item.key === key)
|
|
||||||
|
|
||||||
// index -> key
|
|
||||||
const getKeyByIndex = (index: number) =>
|
|
||||||
data[index]?.key
|
|
||||||
|
|
||||||
|
/* ================= handlers ================= */
|
||||||
const handlePress = (key: TabItem['key']) => {
|
const handlePress = (key: TabItem['key']) => {
|
||||||
setSelectedKey(key)
|
setSelectedKey(key)
|
||||||
onChange?.(key)
|
onChange?.(key)
|
||||||
|
scrollToCenter(key)
|
||||||
if (hasContent) {
|
|
||||||
const index = getIndexByKey(key)
|
|
||||||
if (index !== -1)
|
|
||||||
pagerRef.current?.setPage(index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPageSelected = (e: any) => {
|
const scrollToCenter = (key: TabItem['key']) => {
|
||||||
const index = e.nativeEvent.position
|
const layout = itemLayouts.current[key]
|
||||||
const key = getKeyByIndex(index)
|
if (!layout || !scrollRef.current)
|
||||||
|
return
|
||||||
|
|
||||||
if (key !== undefined) {
|
const offset
|
||||||
setSelectedKey(key)
|
= layout.x + layout.width / 2 - containerWidth.current / 2
|
||||||
onChange?.(key)
|
|
||||||
}
|
scrollRef.current.scrollTo({
|
||||||
|
x: Math.max(0, offset),
|
||||||
|
animated: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================= effects ================= */
|
||||||
|
useEffect(() => {
|
||||||
|
// 首次挂载后,让默认选中项居中
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
scrollToCenter(selectedKey)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/* ================= render ================= */
|
||||||
return (
|
return (
|
||||||
<View className="flex-1">
|
<ScrollView
|
||||||
{/* tab bar */}
|
ref={scrollRef}
|
||||||
<ScrollView
|
horizontal
|
||||||
horizontal
|
showsHorizontalScrollIndicator={false}
|
||||||
showsHorizontalScrollIndicator={false}
|
keyboardShouldPersistTaps="handled"
|
||||||
contentContainerStyle={{
|
onLayout={(e: LayoutChangeEvent) => {
|
||||||
flexDirection: 'row',
|
containerWidth.current = e.nativeEvent.layout.width
|
||||||
gap: 8,
|
}}
|
||||||
}}
|
contentContainerStyle={{
|
||||||
>
|
flexDirection: 'row',
|
||||||
{data.map((item) => {
|
gap: 8,
|
||||||
const isActive = item.key === selectedKey
|
}}
|
||||||
return (
|
>
|
||||||
<Pressable
|
{data.map((item) => {
|
||||||
key={item.key}
|
const isActive = item.key === selectedKey
|
||||||
onPress={() => handlePress(item.key)}
|
|
||||||
|
return (
|
||||||
|
<TouchableWithoutFeedback
|
||||||
|
key={item.key}
|
||||||
|
onPress={() => handlePress(item.key)}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
onLayout={(e) => {
|
||||||
|
const { x, width } = e.nativeEvent.layout
|
||||||
|
itemLayouts.current[item.key] = { x, width }
|
||||||
|
}}
|
||||||
className={
|
className={
|
||||||
isActive
|
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-800'
|
||||||
: 'items-center justify-center rounded-md px-4 py-2 bg-gray-200'
|
: '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>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
{hasContent && (
|
|
||||||
<PagerView
|
|
||||||
ref={pagerRef}
|
|
||||||
style={{ flex: 1, marginTop: 16 }}
|
|
||||||
initialPage={getIndexByKey(initialKey)}
|
|
||||||
onPageSelected={onPageSelected}
|
|
||||||
>
|
|
||||||
{data.map(item => (
|
|
||||||
<View key={item.key} style={{ flex: 1 }}>
|
|
||||||
{item.content}
|
|
||||||
</View>
|
</View>
|
||||||
))}
|
</TouchableWithoutFeedback>
|
||||||
</PagerView>
|
)
|
||||||
)}
|
})}
|
||||||
</View>
|
</ScrollView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import Button from './Button'
|
|||||||
import { Cell, CellGroup } from './Cell'
|
import { Cell, CellGroup } from './Cell'
|
||||||
import Checkbox from './Checkbox'
|
import Checkbox from './Checkbox'
|
||||||
|
|
||||||
|
import DateMorePicker from './DateMorePicker'
|
||||||
import DatePicker from './DatePicker'
|
import DatePicker from './DatePicker'
|
||||||
import DateRangePicker from './DateRangePicker'
|
import DateRangePicker from './DateRangePicker'
|
||||||
import Divider from './Divider'
|
import Divider from './Divider'
|
||||||
@@ -22,6 +23,7 @@ export {
|
|||||||
Cell as ACell,
|
Cell as ACell,
|
||||||
CellGroup as ACellGroup,
|
CellGroup as ACellGroup,
|
||||||
Checkbox as ACheckbox,
|
Checkbox as ACheckbox,
|
||||||
|
DateMorePicker as ADateMorePicker,
|
||||||
DatePicker as ADatePicker,
|
DatePicker as ADatePicker,
|
||||||
DateRangePicker as ADateRangePicker,
|
DateRangePicker as ADateRangePicker,
|
||||||
Divider as ADivider,
|
Divider as ADivider,
|
||||||
|
|||||||
Reference in new issue
Block a user