diff --git a/app/(tabs)/fasting.tsx b/app/(tabs)/fasting.tsx index 638693e..452d15a 100644 --- a/app/(tabs)/fasting.tsx +++ b/app/(tabs)/fasting.tsx @@ -48,6 +48,7 @@ import { import { Ionicons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; +import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { useRouter } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; @@ -592,6 +593,38 @@ export default function FastingTabScreen() { activePlanId={activePlan?.id ?? currentPlan?.id} onSelectPlan={handleSelectPlan} /> + + {/* 参考文献入口 */} + + router.push(ROUTES.FASTING_REFERENCES)} + activeOpacity={0.8} + > + {isLiquidGlassAvailable() ? ( + + + + 参考文献与医学来源 + + + + ) : ( + + + + 参考文献与医学来源 + + + + )} + + (null); const celebrationTimerRef = useRef | null>(null); const [isCelebrationVisible, setIsCelebrationVisible] = useState(false); + const [disclaimerVisible, setDisclaimerVisible] = useState(false); // 从 Redux 获取数据 const selectedKey = selectedDate.format('YYYY-MM-DD'); const medicationsForDay = useAppSelector((state) => selectMedicationDisplayItemsByDate(selectedKey)(state)); const handleOpenAddMedication = useCallback(() => { + // 检查是否已经读过免责声明 + const hasRead = getItemSync(MEDICAL_DISCLAIMER_READ_KEY); + + if (hasRead === 'true') { + // 已读过,直接跳转 + router.push('/medications/add-medication'); + } else { + // 未读过,显示医疗免责声明弹窗 + setDisclaimerVisible(true); + } + }, []); + + const handleDisclaimerConfirm = useCallback(() => { + // 用户同意免责声明后,记录已读状态,关闭弹窗并跳转到添加页面 + setItemSync(MEDICAL_DISCLAIMER_READ_KEY, 'true'); + setDisclaimerVisible(false); router.push('/medications/add-medication'); }, []); + const handleDisclaimerClose = useCallback(() => { + // 用户不接受免责声明,只关闭弹窗,不跳转,不记录已读状态 + setDisclaimerVisible(false); + }, []); + const handleOpenMedicationManagement = useCallback(() => { router.push('/medications/manage-medications'); }, []); @@ -328,6 +355,13 @@ export default function MedicationsScreen() { )} + + {/* 医疗免责声明弹窗 */} + ); } diff --git a/app/(tabs)/personal.tsx b/app/(tabs)/personal.tsx index 51a5d53..babbc24 100644 --- a/app/(tabs)/personal.tsx +++ b/app/(tabs)/personal.tsx @@ -435,6 +435,16 @@ export default function PersonalScreen() { }, ], }, + { + title: t('personal.sections.medicalSources'), + items: [ + { + icon: 'medkit-outline' as React.ComponentProps['name'], + title: t('personal.menu.whoSource'), + onPress: () => Linking.openURL('https://www.who.int'), + }, + ], + }, { title: t('personal.language.title'), items: [ diff --git a/app/fasting/references.tsx b/app/fasting/references.tsx new file mode 100644 index 0000000..5b1dc2d --- /dev/null +++ b/app/fasting/references.tsx @@ -0,0 +1,300 @@ +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; +import { Ionicons } from '@expo/vector-icons'; +import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; +import { useRouter } from 'expo-router'; +import React from 'react'; +import { Linking, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +// 参考文献数据 +const references = [ + { + id: 5, + name: '中国国家卫生健康委员会(国家卫健委)', + englishName: 'National Health Commission of the People\'s Republic of China', + url: 'http://www.nhc.gov.cn', + note: '(用于中文用户环境非常合适)', + }, + { + id: 1, + name: '美国国立卫生研究院(NIH)', + englishName: 'National Institutes of Health', + url: 'https://www.nih.gov', + }, + { + id: 3, + name: '世界卫生组织(WHO)', + englishName: 'World Health Organization', + url: 'https://www.who.int', + }, + + { + id: 6, + name: '中国营养学会(Chinese Nutrition Society)', + englishName: 'Chinese Nutrition Society', + url: 'https://www.cnsoc.org', + }, +]; + +export default function FastingReferencesScreen() { + const router = useRouter(); + const insets = useSafeAreaInsets(); + const theme = useColorScheme() ?? 'light'; + const colors = Colors[theme]; + const glassAvailable = isLiquidGlassAvailable(); + + const handleBack = () => { + router.back(); + }; + + const handleLinkPress = async (url: string) => { + try { + const canOpen = await Linking.canOpenURL(url); + if (canOpen) { + await Linking.openURL(url); + } else { + console.log('无法打开链接:', url); + } + } catch (error) { + console.error('打开链接时发生错误:', error); + } + }; + + return ( + + {/* 固定悬浮的返回按钮 */} + + + {glassAvailable ? ( + + + + ) : ( + + + + )} + + + + + + 参考文献与医学来源 + + 本应用的断食相关功能和建议基于以下权威医学机构的科学研究和指导原则 + + + + + {references.map((reference) => ( + + + + + + + {reference.name} + {reference.englishName} + + + + handleLinkPress(reference.url)} + activeOpacity={0.8} + > + {reference.url} + + + + {reference.note && ( + {reference.note} + )} + + ))} + + + + + + 重要声明 + + + 本应用提供的断食相关信息仅供参考,不能替代专业医疗建议。在开始任何断食计划前, + 请咨询医生或专业医疗人员的意见,特别是如果您有基础疾病、正在服药或处于特殊生理时期(如怀孕、哺乳期等)。 + + + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + }, + backButtonContainer: { + position: 'absolute', + top: 0, + left: 24, + zIndex: 10, + }, + backButton: { + width: 44, + height: 44, + borderRadius: 22, + alignItems: 'center', + justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 4, + }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 8, + }, + backButtonGlass: { + width: 44, + height: 44, + borderRadius: 22, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.3)', + overflow: 'hidden', + }, + backButtonFallback: { + width: 44, + height: 44, + borderRadius: 22, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(255,255,255,0.85)', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.5)', + }, + scrollContainer: { + paddingHorizontal: 24, + paddingBottom: 40, + }, + headerSection: { + alignItems: 'center', + marginBottom: 32, + }, + title: { + fontSize: 28, + fontWeight: '800', + color: '#2E3142', + marginBottom: 12, + textAlign: 'center', + }, + subtitle: { + fontSize: 16, + color: '#6F7D87', + textAlign: 'center', + lineHeight: 24, + paddingHorizontal: 20, + }, + referencesList: { + marginBottom: 32, + }, + referenceCard: { + backgroundColor: '#FFFFFF', + borderRadius: 20, + padding: 20, + marginBottom: 16, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 8, + }, + shadowOpacity: 0.06, + shadowRadius: 16, + elevation: 4, + }, + referenceHeader: { + flexDirection: 'row', + alignItems: 'flex-start', + marginBottom: 12, + }, + referenceIcon: { + width: 48, + height: 48, + borderRadius: 24, + backgroundColor: 'rgba(46, 49, 66, 0.08)', + alignItems: 'center', + justifyContent: 'center', + marginRight: 16, + }, + referenceInfo: { + flex: 1, + }, + referenceName: { + fontSize: 16, + fontWeight: '700', + color: '#2E3142', + marginBottom: 4, + }, + referenceEnglishName: { + fontSize: 14, + color: '#6F7D87', + lineHeight: 20, + }, + referenceLink: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: 'rgba(111, 125, 135, 0.08)', + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 12, + marginBottom: 8, + }, + referenceUrl: { + fontSize: 14, + color: '#2E3142', + flex: 1, + }, + referenceNote: { + fontSize: 13, + color: '#8A96A3', + fontStyle: 'italic', + lineHeight: 18, + }, + disclaimerSection: { + backgroundColor: 'rgba(255, 248, 225, 0.6)', + borderRadius: 20, + padding: 20, + borderWidth: 1, + borderColor: 'rgba(255, 193, 7, 0.2)', + }, + disclaimerHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 12, + }, + disclaimerTitle: { + fontSize: 16, + fontWeight: '700', + color: '#2E3142', + marginLeft: 8, + }, + disclaimerText: { + fontSize: 14, + color: '#5B6572', + lineHeight: 22, + }, +}); \ No newline at end of file diff --git a/components/ui/MedicalDisclaimerSheet.tsx b/components/ui/MedicalDisclaimerSheet.tsx new file mode 100644 index 0000000..c528d8c --- /dev/null +++ b/components/ui/MedicalDisclaimerSheet.tsx @@ -0,0 +1,314 @@ +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 ( + + + + + + + + + + {/* 图标和标题 - 左对齐单行 */} + + + + + 重要提示 + + + {/* 免责声明内容 */} + + + + + 本应用提供的任何禁食、饮食、健康或医学相关内容仅用于一般性信息参考,不构成医疗建议。 + + + + + + + 应用不提供诊断、治疗或医疗服务。 + + + + + + + 如您有任何医疗状况、正在服药、怀孕或哺乳,或计划开始禁食,请先咨询医生或持牌医疗专业人员。 + + + + + + + 如果在禁食或使用本应用过程中出现不适症状,请立即停止并寻求专业医疗帮助。 + + + + + {/* 确认按钮 - 支持 Liquid Glass */} + + + {isLiquidGlassAvailable() ? ( + + {loading ? ( + + ) : ( + <> + + 已读并前往 + + )} + + ) : ( + + {loading ? ( + + ) : ( + <> + + 已读并前往 + + )} + + )} + + + + + + ); +} + +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', + }, +}); \ No newline at end of file diff --git a/constants/Routes.ts b/constants/Routes.ts index 83ce93f..a328439 100644 --- a/constants/Routes.ts +++ b/constants/Routes.ts @@ -57,6 +57,7 @@ export const ROUTES = { // 轻断食相关 FASTING_PLAN_DETAIL: '/fasting', + FASTING_REFERENCES: '/fasting/references', // 新用户引导 ONBOARDING: '/onboarding', diff --git a/i18n/index.ts b/i18n/index.ts index 86ed8ec..e4013c5 100644 --- a/i18n/index.ts +++ b/i18n/index.ts @@ -38,6 +38,7 @@ const personalScreenResources = { account: '账号与安全', language: '语言', healthData: '健康数据授权', + medicalSources: '医学建议来源', }, menu: { notificationSettings: '通知设置', @@ -49,6 +50,7 @@ const personalScreenResources = { logout: '退出登录', deleteAccount: '注销帐号', healthDataPermissions: '健康数据授权说明', + whoSource: '世界卫生组织 (WHO)', }, language: { title: '语言', @@ -783,6 +785,7 @@ const resources = { account: 'Account & Security', language: 'Language', healthData: 'Health data permissions', + medicalSources: 'Medical Advice Sources', }, menu: { notificationSettings: 'Notification settings', @@ -794,6 +797,7 @@ const resources = { logout: 'Log out', deleteAccount: 'Delete account', healthDataPermissions: 'Health data disclosure', + whoSource: 'World Health Organization (WHO)', }, language: { title: 'Language',