import CelebrationAnimation, { CelebrationAnimationRef } from '@/components/CelebrationAnimation'; import { DateSelector } from '@/components/DateSelector'; import { MedicationAddOptionsSheet } from '@/components/medication/MedicationAddOptionsSheet'; import { MedicationCard } from '@/components/medication/MedicationCard'; import { TakenMedicationsStack } from '@/components/medication/TakenMedicationsStack'; import { ThemedText } from '@/components/ThemedText'; import { IconSymbol } from '@/components/ui/IconSymbol'; import { MedicalDisclaimerSheet } from '@/components/ui/MedicalDisclaimerSheet'; import { Colors } from '@/constants/Colors'; import { useMembershipModal } from '@/contexts/MembershipModalContext'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useVipService } from '@/hooks/useVipService'; import { fetchMedicationRecords, fetchMedications, selectMedicationDisplayItemsByDate } from '@/store/medicationsSlice'; import { DEFAULT_MEMBER_NAME } from '@/store/userSlice'; import { getItemSync, setItemSync } from '@/utils/kvStore'; import { convertMedicationDataToWidget, refreshWidget, syncMedicationDataToWidget } from '@/utils/widgetDataSync'; import { useFocusEffect } from '@react-navigation/native'; import dayjs, { Dayjs } from 'dayjs'; import 'dayjs/locale/zh-cn'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { router } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ScrollView, StyleSheet, TouchableOpacity, View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; dayjs.locale('zh-cn'); // 本地存储键名:医疗免责声明已读状态 const MEDICAL_DISCLAIMER_READ_KEY = 'medical_disclaimer_read'; type MedicationFilter = 'all' | 'taken' | 'missed'; type ThemeColors = (typeof Colors)[keyof typeof Colors]; export default function MedicationsScreen() { const { t } = useTranslation(); const dispatch = useAppDispatch(); const insets = useSafeAreaInsets(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colors: ThemeColors = Colors[theme]; const userProfile = useAppSelector((state) => state.user.profile); const { ensureLoggedIn } = useAuthGuard(); const { checkServiceAccess } = useVipService(); const { openMembershipModal } = useMembershipModal(); const [selectedDate, setSelectedDate] = useState(dayjs()); const [selectedDateIndex, setSelectedDateIndex] = useState(selectedDate.date() - 1); const [activeFilter, setActiveFilter] = useState('all'); const celebrationRef = useRef(null); const celebrationTimerRef = useRef | null>(null); const [isCelebrationVisible, setIsCelebrationVisible] = useState(false); const [disclaimerVisible, setDisclaimerVisible] = useState(false); const [addSheetVisible, setAddSheetVisible] = useState(false); const [pendingAction, setPendingAction] = useState<'manual' | null>(null); // 从 Redux 获取数据 const selectedKey = selectedDate.format('YYYY-MM-DD'); // 使用 useMemo 缓存 selector 实例,避免每次渲染都创建新的 selector const medicationSelector = useMemo( () => selectMedicationDisplayItemsByDate(selectedKey), [selectedKey] ); const medicationsForDay = useAppSelector(medicationSelector); const handleOpenAddSheet = useCallback(() => { setAddSheetVisible(true); }, []); const handleManualAdd = useCallback(() => { const hasRead = getItemSync(MEDICAL_DISCLAIMER_READ_KEY); setPendingAction('manual'); if (hasRead === 'true') { setAddSheetVisible(false); setPendingAction(null); router.push('/medications/add-medication'); } else { setAddSheetVisible(false); setDisclaimerVisible(true); } }, []); const handleAiRecognize = useCallback(async () => { setAddSheetVisible(false); const isLoggedIn = await ensureLoggedIn(); if (!isLoggedIn) return; const access = checkServiceAccess(); if (!access.canUseService) { openMembershipModal(); return; } router.push('/medications/ai-camera'); }, [checkServiceAccess, ensureLoggedIn, openMembershipModal, router]); const handleDisclaimerConfirm = useCallback(() => { // 用户同意免责声明后,记录已读状态,关闭弹窗并跳转到添加页面 setItemSync(MEDICAL_DISCLAIMER_READ_KEY, 'true'); setDisclaimerVisible(false); if (pendingAction === 'manual') { setPendingAction(null); router.push('/medications/add-medication'); } }, [pendingAction]); const handleDisclaimerClose = useCallback(() => { // 用户不接受免责声明,只关闭弹窗,不跳转,不记录已读状态 setDisclaimerVisible(false); setPendingAction(null); }, []); const handleOpenMedicationManagement = useCallback(() => { router.push('/medications/manage-medications'); }, []); const handleOpenMedicationDetails = useCallback((medicationId: string) => { router.push({ pathname: '/medications/[medicationId]', params: { medicationId }, }); }, []); const handleMedicationTakenCelebration = useCallback(() => { if (celebrationTimerRef.current) { clearTimeout(celebrationTimerRef.current); } setIsCelebrationVisible(true); requestAnimationFrame(() => { celebrationRef.current?.play(); }); celebrationTimerRef.current = setTimeout(() => { setIsCelebrationVisible(false); }, 2400); }, []); // 加载药物和记录数据 useEffect(() => { dispatch(fetchMedications()); dispatch(fetchMedicationRecords({ date: selectedKey })); }, [dispatch, selectedKey]); useEffect(() => { return () => { if (celebrationTimerRef.current) { clearTimeout(celebrationTimerRef.current); } }; }, []); // 页面聚焦时刷新数据,确保从添加页面返回时能看到最新数据 useFocusEffect( useCallback(() => { // 重新安排药品通知并刷新数据 const refreshDataAndRescheduleNotifications = async () => { try { // 只获取一次药物数据,然后复用结果 const medications = await dispatch(fetchMedications({ isActive: true })).unwrap(); // 获取药物记录 const recordsAction = await dispatch(fetchMedicationRecords({ date: selectedKey })); // 同步数据到小组件(仅同步今天的) const today = dayjs().format('YYYY-MM-DD'); const records = recordsAction.payload as any; if (selectedKey === today && records?.records) { const medicationData = convertMedicationDataToWidget( records.records, medications, selectedKey ); await syncMedicationDataToWidget(medicationData); // 刷新小组件 await refreshWidget(); } } catch (error) { console.error('刷新数据或重新安排药品通知失败:', error); } }; refreshDataAndRescheduleNotifications(); }, [dispatch, selectedKey]) ); useEffect(() => { setActiveFilter('all'); }, [selectedDate]); // 为每个药物添加默认图片(如果没有图片) const medicationsWithImages = useMemo(() => { return medicationsForDay.map((med: any) => ({ ...med, image: med.image || require('@/assets/images/medicine/image-medicine.png'), // 默认使用瓶子图标 })); }, [medicationsForDay]); const filteredMedications = useMemo(() => { if (activeFilter === 'all') { return medicationsWithImages; } // "未服用" tab 包含 missed(已错过)和 upcoming(待服用)两种状态 if (activeFilter === 'missed') { return medicationsWithImages.filter((item: any) => item.status === 'missed' || item.status === 'upcoming' ); } // 其他状态按原逻辑过滤 return medicationsWithImages.filter((item: any) => item.status === activeFilter); }, [activeFilter, medicationsWithImages]); const activeMedications = useMemo(() => { if (activeFilter !== 'all') return filteredMedications; return filteredMedications.filter((item: any) => item.status !== 'taken' && item.status !== 'skipped'); }, [activeFilter, filteredMedications]); const completedMedications = useMemo(() => { if (activeFilter !== 'all') return []; return filteredMedications.filter((item: any) => item.status === 'taken' || item.status === 'skipped'); }, [activeFilter, filteredMedications]); const counts = useMemo(() => { const taken = medicationsWithImages.filter((item: any) => item.status === 'taken').length; // "未服用"计数包含 missed(已错过)和 upcoming(待服用) const missed = medicationsWithImages.filter((item: any) => item.status === 'missed' || item.status === 'upcoming' ).length; return { all: medicationsWithImages.length, taken, missed, }; }, [medicationsWithImages]); const displayName = userProfile.name?.trim() || DEFAULT_MEMBER_NAME; const headerDateLabel = selectedDate.isSame(dayjs(), 'day') ? t('medications.dateFormats.today', { date: selectedDate.format('M月D日') }) : t('medications.dateFormats.other', { date: selectedDate.format('M月D日 dddd') }); const emptyState = filteredMedications.length === 0; return ( {isCelebrationVisible ? ( ) : null} {/* 背景渐变 */} {/* 装饰性圆圈 */} {t('medications.greeting', { name: displayName })} {t('medications.welcome')} {isLiquidGlassAvailable() ? ( ) : ( )} {isLiquidGlassAvailable() ? ( ) : ( )} { setSelectedDate(dayjs(date)); setSelectedDateIndex(index); }} disableFutureDates containerStyle={styles.dateSelectorContainer} /> {t('medications.todayMedications')} {(['all', 'taken', 'missed'] as MedicationFilter[]).map((filter) => { const isActive = activeFilter === filter; return ( setActiveFilter(filter)} style={[ styles.segment, isActive && { backgroundColor: colors.primary }, ]} > {t(`medications.filters.${filter}`)} {counts[filter]} ); })} {emptyState ? ( {t('medications.emptyState.title')} {t('medications.emptyState.subtitle')} ) : ( {/* 渲染未服用的药物 */} {activeMedications.map((item: any) => ( handleOpenMedicationDetails(item.medicationId)} onCelebrate={handleMedicationTakenCelebration} /> ))} {/* 渲染已完成(服用/跳过)的药物堆叠 */} {completedMedications.length > 0 && ( handleOpenMedicationDetails(item.medicationId)} onCelebrate={handleMedicationTakenCelebration} /> )} )} setAddSheetVisible(false)} onManualAdd={handleManualAdd} onAiRecognize={handleAiRecognize} /> {/* 医疗免责声明弹窗 */} ); } const styles = StyleSheet.create({ container: { flex: 1, }, gradientBackground: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, decorativeCircle1: { position: 'absolute', top: 40, right: 20, width: 60, height: 60, borderRadius: 30, backgroundColor: '#0EA5E9', opacity: 0.1, }, decorativeCircle2: { position: 'absolute', bottom: -15, left: -15, width: 40, height: 40, borderRadius: 20, backgroundColor: '#0EA5E9', opacity: 0.05, }, scrollContent: { paddingHorizontal: 20, gap: 24, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, headerActions: { flexDirection: 'row', alignItems: 'center', gap: 10, }, headerAddButton: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center', overflow: 'hidden', }, fallbackAddButton: { backgroundColor: 'rgba(255, 255, 255, 0.9)', borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.3)', }, avatar: { width: 60, height: 60, borderRadius: 30, }, greeting: { fontSize: 24, fontWeight: '600', }, welcome: { marginTop: 6, fontSize: 14, }, sectionSpacing: { gap: 16, }, dateSelectorContainer: { paddingRight: 0, }, sectionTitle: { fontSize: 16, fontWeight: '500', }, sectionHeader: { fontSize: 20, fontWeight: '600', }, segmentedControl: { flexDirection: 'row', borderRadius: 18, padding: 6, gap: 6, }, segment: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, borderRadius: 14, paddingVertical: 10, }, segmentLabel: { fontSize: 14, fontWeight: '600', }, segmentBadge: { minWidth: 24, height: 24, borderRadius: 12, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 6, }, segmentBadgeText: { fontSize: 12, fontWeight: '600', }, emptyState: { alignItems: 'center', paddingHorizontal: 24, paddingVertical: 32, borderRadius: 24, gap: 16, }, emptyIllustration: { width: 160, height: 160, resizeMode: 'contain', }, emptyTitle: { textAlign: 'center', fontSize: 18, fontWeight: '600', }, emptySubtitle: { textAlign: 'center', fontSize: 14, lineHeight: 20, }, primaryButton: { marginTop: 8, paddingVertical: 14, paddingHorizontal: 32, borderRadius: 22, flexDirection: 'row', alignItems: 'center', gap: 8, }, primaryButtonText: { fontSize: 16, fontWeight: '600', }, cardsWrapper: { gap: 16, }, loadingContainer: { alignItems: 'center', paddingHorizontal: 24, paddingVertical: 48, borderRadius: 24, gap: 16, }, loadingText: { fontSize: 14, }, });