import { Ionicons } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; import React, { useEffect, useRef } from 'react'; import { Animated, Dimensions, Modal, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; const { height: screenHeight } = Dimensions.get('window'); export interface ActionSheetOption { id: string; title: string; subtitle?: string; icon?: keyof typeof Ionicons.glyphMap; iconColor?: string; destructive?: boolean; onPress: () => void; } interface ActionSheetProps { visible: boolean; onClose: () => void; title?: string; subtitle?: string; options: ActionSheetOption[]; cancelText?: string; } export function ActionSheet({ visible, onClose, title, subtitle, options, cancelText = '取消' }: ActionSheetProps) { const slideAnim = useRef(new Animated.Value(screenHeight)).current; const opacityAnim = useRef(new Animated.Value(0)).current; useEffect(() => { if (visible) { // 显示动画 Animated.parallel([ Animated.timing(slideAnim, { toValue: 0, duration: 300, useNativeDriver: true, }), Animated.timing(opacityAnim, { toValue: 1, duration: 200, useNativeDriver: true, }), ]).start(); } else { // 隐藏动画 Animated.parallel([ Animated.timing(slideAnim, { toValue: screenHeight, duration: 250, useNativeDriver: true, }), Animated.timing(opacityAnim, { toValue: 0, duration: 150, useNativeDriver: true, }), ]).start(); } }, [visible]); const handleOptionPress = (option: ActionSheetOption) => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); onClose(); // 延迟执行选项回调,让关闭动画先完成 setTimeout(() => { option.onPress(); }, 100); }; const handleCancel = () => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); onClose(); }; if (!visible) return null; return ( {/* 背景遮罩 */} {/* 弹窗内容 */} {/* 拖拽指示器 */} {/* 标题区域 */} {(title || subtitle) && ( {title && {title}} {subtitle && {subtitle}} )} {/* 选项列表 */} {options.map((option, index) => ( handleOptionPress(option)} activeOpacity={0.7} > {option.icon && ( )} {option.title} {option.subtitle && ( {option.subtitle} )} ))} {/* 取消按钮 */} {cancelText} {/* 底部安全区域 */} ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'flex-end', }, backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.4)', }, sheet: { backgroundColor: '#FFFFFF', borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingTop: 8, maxHeight: screenHeight * 0.8, }, dragIndicator: { width: 36, height: 4, backgroundColor: '#E5E7EB', borderRadius: 2, alignSelf: 'center', marginBottom: 16, }, header: { paddingHorizontal: 20, paddingBottom: 16, borderBottomWidth: 1, borderBottomColor: '#F3F4F6', }, title: { fontSize: 18, fontWeight: '700', color: '#111827', textAlign: 'center', marginBottom: 4, }, subtitle: { fontSize: 14, color: '#6B7280', textAlign: 'center', lineHeight: 20, }, optionsContainer: { paddingHorizontal: 20, paddingTop: 8, }, option: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 16, paddingHorizontal: 16, backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#E5E7EB', marginBottom: 1, }, firstOption: { borderTopLeftRadius: 12, borderTopRightRadius: 12, }, lastOption: { borderBottomLeftRadius: 12, borderBottomRightRadius: 12, marginBottom: 0, }, optionContent: { flexDirection: 'row', alignItems: 'center', flex: 1, }, iconContainer: { width: 32, height: 32, borderRadius: 8, backgroundColor: '#FFFFFF', alignItems: 'center', justifyContent: 'center', marginRight: 12, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 2, shadowOffset: { width: 0, height: 1 }, elevation: 1, }, textContainer: { flex: 1, }, optionTitle: { fontSize: 16, fontWeight: '600', color: '#111827', marginBottom: 2, }, optionSubtitle: { fontSize: 13, color: '#6B7280', lineHeight: 18, }, destructiveText: { color: '#EF4444', }, cancelButton: { marginHorizontal: 20, marginTop: 16, paddingVertical: 16, backgroundColor: '#F3F4F6', borderRadius: 12, alignItems: 'center', }, cancelText: { fontSize: 16, fontWeight: '600', color: '#374151', }, safeBottom: { height: 34, // iPhone底部安全区域高度 }, });