Files
digital-pilates/components/ui/MedicalDisclaimerSheet.tsx
richarjiang 6f2b7eb45e feat(medications): 简化药品添加流程并优化AI相机交互体验
- 移除药品添加选项底部抽屉,直接跳转至AI识别相机
- 优化AI相机拍摄完成后的按钮交互,展开为"拍照"和"完成"两个按钮
- 添加相机引导提示本地存储,避免重复显示
- 修复相机页面布局跳动问题,固定相机高度
- 为医疗免责声明组件添加触觉反馈错误处理
- 实现活动热力图的国际化支持,包括月份标签和统计文本
2025-11-25 14:09:24 +08:00

320 lines
8.5 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).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',
},
});