Files
digital-pilates/components/ui/MedicalDisclaimerSheet.tsx
richarjiang 6ad77bc0e2 feat(medical): 添加医疗免责声明和参考文献功能
- 在用药模块首次添加时显示医疗免责声明弹窗
- 新增断食参考文献页面,展示权威医学机构来源
- 在个人中心添加WHO医学来源入口
- 使用本地存储记录用户已读免责声明状态
- 支持Liquid Glass毛玻璃效果和降级方案
- 新增中英文国际化翻译支持
2025-11-14 09:14:12 +08:00

314 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Ionicons } from '@expo/vector-icons';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
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 MedicalDisclaimerSheetProps {
visible: boolean;
onClose: () => void;
onConfirm: () => void;
loading?: boolean;
}
/**
* 医疗免责声明弹窗组件
* 用于在用户添加药品前显示医疗免责声明
*/
export function MedicalDisclaimerSheet({
visible,
onClose,
onConfirm,
loading = false,
}: MedicalDisclaimerSheetProps) {
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(Haptics.NotificationFeedbackType.Success);
onConfirm();
};
if (!modalVisible) {
return null;
}
return (
<Modal
visible={modalVisible}
transparent
animationType="none"
onRequestClose={onClose}
statusBarTranslucent
>
<View 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} />
{/* 图标和标题 - 左对齐单行 */}
<View style={styles.header}>
<View style={styles.iconContainer}>
<Ionicons name="information-circle" size={24} color="#3B82F6" />
</View>
<Text style={styles.title}></Text>
</View>
{/* 免责声明内容 */}
<View style={styles.contentContainer}>
<View style={styles.disclaimerItem}>
<View style={styles.bulletPoint} />
<Text style={styles.disclaimerText}>
</Text>
</View>
<View style={styles.disclaimerItem}>
<View style={styles.bulletPoint} />
<Text style={styles.disclaimerText}>
</Text>
</View>
<View style={styles.disclaimerItem}>
<View style={styles.bulletPoint} />
<Text style={styles.disclaimerText}>
怀
</Text>
</View>
<View style={styles.disclaimerItem}>
<View style={styles.bulletPoint} />
<Text style={styles.disclaimerText}>
使
</Text>
</View>
</View>
{/* 确认按钮 - 支持 Liquid Glass */}
<View style={styles.actions}>
<TouchableOpacity
activeOpacity={0.9}
onPress={handleConfirm}
disabled={loading}
>
{isLiquidGlassAvailable() ? (
<GlassView
style={styles.confirmButton}
glassEffectStyle="regular"
tintColor="rgba(59, 130, 246, 0.8)"
isInteractive={true}
>
{loading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<>
<Ionicons name="checkmark-circle" size={20} color="#fff" />
<Text style={styles.confirmText}></Text>
</>
)}
</GlassView>
) : (
<View style={[styles.confirmButton, styles.fallbackButton]}>
{loading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<>
<Ionicons name="checkmark-circle" size={20} color="#fff" />
<Text style={styles.confirmText}></Text>
</>
)}
</View>
)}
</TouchableOpacity>
</View>
</Animated.View>
</View>
</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: 20,
},
handle: {
width: 50,
height: 4,
borderRadius: 2,
backgroundColor: '#E5E7EB',
alignSelf: 'center',
marginBottom: 8,
},
header: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
iconContainer: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#EFF6FF',
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 20,
fontWeight: '700',
color: '#111827',
},
contentContainer: {
gap: 16,
paddingVertical: 8,
},
disclaimerItem: {
flexDirection: 'row',
gap: 12,
alignItems: 'flex-start',
},
bulletPoint: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: '#3B82F6',
marginTop: 8,
},
disclaimerText: {
flex: 1,
fontSize: 15,
lineHeight: 22,
color: '#374151',
},
actions: {
marginTop: 8,
},
confirmButton: {
height: 56,
borderRadius: 18,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
overflow: 'hidden', // 保证玻璃边界圆角效果
},
fallbackButton: {
backgroundColor: '#3B82F6',
shadowColor: 'rgba(59, 130, 246, 0.45)',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 1,
shadowRadius: 20,
elevation: 6,
},
confirmText: {
fontSize: 16,
fontWeight: '700',
color: '#fff',
},
});