import * as Haptics from 'expo-haptics'; import React, { useEffect, useRef, useState } from 'react'; import { ActivityIndicator, Animated, Dimensions, Modal, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; const { height: screenHeight } = Dimensions.get('window'); interface ConfirmationSheetProps { visible: boolean; onClose: () => void; onConfirm: () => void; title: string; description?: string; confirmText?: string; cancelText?: string; destructive?: boolean; loading?: boolean; } export function ConfirmationSheet({ visible, onClose, onConfirm, title, description, confirmText = '确认', cancelText = '取消', destructive = false, loading = false, }: ConfirmationSheetProps) { const insets = useSafeAreaInsets(); const translateY = useRef(new Animated.Value(screenHeight)).current; const backdropOpacity = useRef(new Animated.Value(0)).current; const [modalVisible, setModalVisible] = useState(visible); useEffect(() => { if (visible) { setModalVisible(true); } }, [visible]); useEffect(() => { if (!modalVisible) { return; } if (visible) { translateY.setValue(screenHeight); backdropOpacity.setValue(0); Animated.parallel([ Animated.timing(backdropOpacity, { toValue: 1, duration: 200, useNativeDriver: true, }), Animated.spring(translateY, { toValue: 0, useNativeDriver: true, bounciness: 6, speed: 12, }), ]).start(); return; } Animated.parallel([ Animated.timing(backdropOpacity, { toValue: 0, duration: 150, useNativeDriver: true, }), Animated.timing(translateY, { toValue: screenHeight, duration: 240, useNativeDriver: true, }), ]).start(() => { translateY.setValue(screenHeight); backdropOpacity.setValue(0); setModalVisible(false); }); }, [visible, modalVisible, backdropOpacity, translateY]); const handleCancel = () => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); onClose(); }; const handleConfirm = () => { if (loading) return; Haptics.notificationAsync( destructive ? Haptics.NotificationFeedbackType.Error : Haptics.NotificationFeedbackType.Success ); onConfirm(); }; if (!modalVisible) { return null; } return ( {title} {description ? {description} : null} {cancelText} {loading ? ( ) : ( {confirmText} )} ); } const styles = StyleSheet.create({ overlay: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'transparent', }, backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(15, 23, 42, 0.45)', }, sheet: { backgroundColor: '#fff', borderTopLeftRadius: 28, borderTopRightRadius: 28, paddingHorizontal: 24, paddingTop: 16, shadowColor: '#000', shadowOpacity: 0.12, shadowRadius: 16, shadowOffset: { width: 0, height: -4 }, elevation: 16, gap: 12, }, handle: { width: 50, height: 4, borderRadius: 2, backgroundColor: '#E5E7EB', alignSelf: 'center', marginBottom: 8, }, title: { fontSize: 18, fontWeight: '700', color: '#111827', textAlign: 'center', }, description: { fontSize: 15, color: '#6B7280', textAlign: 'center', lineHeight: 22, }, actions: { flexDirection: 'row', gap: 12, marginTop: 8, }, cancelButton: { flex: 1, height: 56, borderRadius: 18, borderWidth: 1, borderColor: '#E5E7EB', alignItems: 'center', justifyContent: 'center', backgroundColor: '#F8FAFC', }, cancelText: { fontSize: 16, fontWeight: '600', color: '#111827', }, confirmButton: { flex: 1, height: 56, borderRadius: 18, alignItems: 'center', justifyContent: 'center', shadowColor: 'rgba(239, 68, 68, 0.45)', shadowOffset: { width: 0, height: 10 }, shadowOpacity: 1, shadowRadius: 20, elevation: 6, }, primaryButton: { backgroundColor: '#2563EB', }, destructiveButton: { backgroundColor: '#EF4444', }, disabledButton: { opacity: 0.7, }, confirmText: { fontSize: 16, fontWeight: '700', color: '#fff', }, });