import React, { useEffect, useMemo, useState } from 'react'; import { Modal, Pressable, StyleSheet, Text, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import Animated, { Easing, runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; type Props = { visible: boolean; title?: string; onClose: () => void; children: React.ReactNode; }; /** * 通用底部上拉弹窗(Sheet) * - 内容区背景色固定:#FAF3EC * - 关闭方式:点 X(本期不要求点遮罩关闭) */ export default function SheetModal({ visible, title, onClose, children }: Props) { const insets = useSafeAreaInsets(); const [mounted, setMounted] = useState(false); const progress = useSharedValue(0); // 0: 关闭, 1: 打开 useEffect(() => { if (visible) { setMounted(true); progress.value = withTiming(1, { duration: 260, easing: Easing.out(Easing.cubic) }); return; } if (!mounted) return; progress.value = withTiming( 0, { duration: 220, easing: Easing.in(Easing.cubic) }, (finished) => { if (finished) runOnJS(setMounted)(false); } ); }, [visible, mounted, progress]); const overlayStyle = useAnimatedStyle(() => { return { opacity: 0.4 * progress.value }; }); const sheetStyle = useAnimatedStyle(() => { const translateY = (1 - progress.value) * 380; return { transform: [{ translateY }] }; }); const containerPaddingBottom = useMemo(() => Math.max(insets.bottom, 12), [insets.bottom]); // 注意:Modal 的 visible 必须为 true 才会渲染,因此用 mounted 保持退场动画 return ( {title ?? ''} × {children} ); } const styles = StyleSheet.create({ root: { flex: 1, justifyContent: 'flex-end', }, overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: '#000', }, sheet: { backgroundColor: '#FAF3EC', borderTopLeftRadius: 24, borderTopRightRadius: 24, paddingTop: 14, paddingHorizontal: 16, }, header: { height: 44, justifyContent: 'center', alignItems: 'center', }, title: { color: '#5E2A28', fontSize: 16, fontWeight: '700', }, close: { position: 'absolute', left: 4, top: 0, height: 44, width: 44, alignItems: 'center', justifyContent: 'center', }, closeText: { color: '#5E2A28', fontSize: 28, lineHeight: 28, fontWeight: '400', }, body: { paddingTop: 10, }, });