Files
digital-pilates/components/ui/ConfirmationSheet.tsx
richarjiang 39671ed70f feat(challenges): 添加自定义挑战功能和多语言支持
- 新增自定义挑战创建页面,支持设置挑战类型、时间范围、目标值等
- 实现挑战邀请码系统,支持通过邀请码加入自定义挑战
- 完善挑战详情页面的多语言翻译支持
- 优化用户认证状态检查逻辑,使用token作为主要判断依据
- 添加阿里字体文件支持,提升UI显示效果
- 改进确认弹窗组件,支持Liquid Glass效果和自定义内容
- 优化应用启动流程,直接读取onboarding状态而非预加载用户数据
2025-11-26 16:39:01 +08:00

315 lines
7.7 KiB
TypeScript

import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import * as Haptics from 'expo-haptics';
import React, { useEffect, useRef, useState } from 'react';
import {
ActivityIndicator,
Animated,
Dimensions,
KeyboardAvoidingView,
Modal,
Platform,
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;
content?: React.ReactNode;
}
export function ConfirmationSheet({
visible,
onClose,
onConfirm,
title,
description,
confirmText = '确认',
cancelText = '取消',
destructive = false,
loading = false,
content,
}: 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);
const isGlassAvailable = isLiquidGlassAvailable();
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 (
<Modal
visible={modalVisible}
transparent
animationType="none"
onRequestClose={onClose}
statusBarTranslucent
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
style={styles.overlay}
>
<Animated.View
style={[
styles.backdrop,
{
opacity: backdropOpacity,
},
]}
>
<TouchableOpacity style={StyleSheet.absoluteFillObject} activeOpacity={1} onPress={handleCancel} />
</Animated.View>
<Animated.View
style={[
styles.sheet,
{
transform: [{ translateY }],
paddingBottom: Math.max(insets.bottom, 20),
},
]}
>
<View style={styles.handle} />
<Text style={styles.title}>{title}</Text>
{description ? <Text style={styles.description}>{description}</Text> : null}
{content}
<View style={styles.actions}>
<TouchableOpacity
style={[styles.buttonContainer, loading && styles.disabledButton]}
activeOpacity={0.85}
onPress={handleCancel}
disabled={loading}
>
{isGlassAvailable ? (
<GlassView
style={styles.glassButton}
glassEffectStyle="regular"
tintColor="rgba(241, 245, 249, 0.6)"
isInteractive
>
<Text style={styles.cancelText}>{cancelText}</Text>
</GlassView>
) : (
<View style={styles.cancelButton}>
<Text style={styles.cancelText}>{cancelText}</Text>
</View>
)}
</TouchableOpacity>
<TouchableOpacity
style={[styles.buttonContainer, loading && styles.disabledButton]}
activeOpacity={0.85}
onPress={handleConfirm}
disabled={loading}
>
{isGlassAvailable ? (
<GlassView
style={styles.glassButton}
glassEffectStyle="regular"
tintColor={destructive ? 'rgba(239, 68, 68, 0.85)' : 'rgba(37, 99, 235, 0.85)'}
isInteractive
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.confirmText}>{confirmText}</Text>
)}
</GlassView>
) : (
<View
style={[
styles.confirmButton,
destructive ? styles.destructiveButton : styles.primaryButton,
]}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.confirmText}>{confirmText}</Text>
)}
</View>
)}
</TouchableOpacity>
</View>
</Animated.View>
</KeyboardAvoidingView>
</Modal>
);
}
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,
},
buttonContainer: {
flex: 1,
},
glassButton: {
height: 56,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
cancelButton: {
height: 56,
borderRadius: 18,
borderWidth: 1,
borderColor: '#E5E7EB',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F8FAFC',
},
cancelText: {
fontSize: 16,
fontWeight: '600',
color: '#111827',
},
confirmButton: {
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',
},
});