- 移除药品添加选项底部抽屉,直接跳转至AI识别相机 - 优化AI相机拍摄完成后的按钮交互,展开为"拍照"和"完成"两个按钮 - 添加相机引导提示本地存储,避免重复显示 - 修复相机页面布局跳动问题,固定相机高度 - 为医疗免责声明组件添加触觉反馈错误处理 - 实现活动热力图的国际化支持,包括月份标签和统计文本
320 lines
8.5 KiB
TypeScript
320 lines
8.5 KiB
TypeScript
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).catch((error) => {
|
||
console.warn('[MEDICATION] Haptic feedback failed:', error);
|
||
});
|
||
onClose();
|
||
};
|
||
|
||
const handleConfirm = () => {
|
||
if (loading) return;
|
||
// 安全地执行触觉反馈,避免因触觉反馈失败导致页面卡顿
|
||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch((error) => {
|
||
console.warn('[MEDICATION] Haptic feedback failed:', error);
|
||
});
|
||
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',
|
||
},
|
||
}); |