import { ThemedText } from '@/components/ThemedText'; import { ConfirmationSheet } from '@/components/ui/ConfirmationSheet'; import { HeaderBar } from '@/components/ui/HeaderBar'; import InfoCard from '@/components/ui/InfoCard'; import { Colors } from '@/constants/Colors'; import { DOSAGE_UNITS, DOSAGE_VALUES, FORM_OPTIONS } from '@/constants/Medication'; import { ROUTES } from '@/constants/Routes'; import { useMembershipModal } from '@/contexts/MembershipModalContext'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useI18n } from '@/hooks/useI18n'; import { useVipService } from '@/hooks/useVipService'; import { medicationNotificationService } from '@/services/medicationNotifications'; import { analyzeMedicationStream, getMedicationById, getMedicationRecords, } from '@/services/medications'; import { deleteMedicationAction, fetchMedications, selectMedications, updateMedicationAction, } from '@/store/medicationsSlice'; import type { Medication, MedicationForm } from '@/types/medication'; import { Ionicons } from '@expo/vector-icons'; import { Picker } from '@react-native-picker/picker'; import Voice from '@react-native-voice/voice'; import dayjs from 'dayjs'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { useLocalSearchParams, useRouter } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, Keyboard, Modal, Platform, Pressable, ScrollView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View, } from 'react-native'; import ImageViewing from 'react-native-image-viewing'; import Markdown from 'react-native-markdown-display'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; const DEFAULT_IMAGE = require('@/assets/images/medicine/image-medicine.png'); type RecordsSummary = { takenCount: number; startedDays: number | null; }; export default function MedicationDetailScreen() { const { t } = useI18n(); const params = useLocalSearchParams<{ medicationId?: string }>(); const medicationId = Array.isArray(params.medicationId) ? params.medicationId[0] : params.medicationId; const dispatch = useAppDispatch(); const scheme = (useColorScheme() ?? 'light') as keyof typeof Colors; const colors = Colors[scheme]; const insets = useSafeAreaInsets(); const router = useRouter(); const { ensureLoggedIn } = useAuthGuard(); const { openMembershipModal } = useMembershipModal(); const { checkServiceAccess } = useVipService(); const medications = useAppSelector(selectMedications); const medicationFromStore = medications.find((item) => item.id === medicationId); const [medication, setMedication] = useState(medicationFromStore ?? null); const [loading, setLoading] = useState(!medicationFromStore); const [summary, setSummary] = useState({ takenCount: 0, startedDays: null, }); const [summaryLoading, setSummaryLoading] = useState(true); const [updatePending, setUpdatePending] = useState(false); const [error, setError] = useState(null); const [noteModalVisible, setNoteModalVisible] = useState(false); const [noteDraft, setNoteDraft] = useState(medication?.note ?? ''); const [noteSaving, setNoteSaving] = useState(false); const [dictationActive, setDictationActive] = useState(false); const [dictationLoading, setDictationLoading] = useState(false); const isDictationSupported = Platform.OS === 'ios'; const [keyboardHeight, setKeyboardHeight] = useState(0); const [deleteSheetVisible, setDeleteSheetVisible] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); const [deactivateSheetVisible, setDeactivateSheetVisible] = useState(false); const [deactivateLoading, setDeactivateLoading] = useState(false); const [showImagePreview, setShowImagePreview] = useState(false); // AI 分析相关状态 const [aiAnalysisLoading, setAiAnalysisLoading] = useState(false); const [aiAnalysisContent, setAiAnalysisContent] = useState(''); const [aiAnalysisAbortController, setAiAnalysisAbortController] = useState(null); // 剂量选择相关状态 const [dosagePickerVisible, setDosagePickerVisible] = useState(false); const [dosageValuePicker, setDosageValuePicker] = useState( medicationFromStore?.dosageValue ?? 1 ); const [dosageUnitPicker, setDosageUnitPicker] = useState( medicationFromStore?.dosageUnit ?? DOSAGE_UNITS[0] ); // 剂型选择相关状态 const [formPickerVisible, setFormPickerVisible] = useState(false); const [formPicker, setFormPicker] = useState( medicationFromStore?.form ?? 'capsule' ); // ScrollView 引用,用于滚动到底部 const scrollViewRef = React.useRef(null); useEffect(() => { if (!medicationFromStore) { dispatch(fetchMedications()); } }, [dispatch, medicationFromStore]); useEffect(() => { if (medicationFromStore) { setMedication(medicationFromStore); setLoading(false); // 如果服务端返回了 AI 分析结果,自动展示 if (medicationFromStore.aiAnalysis) { setAiAnalysisContent(medicationFromStore.aiAnalysis); } } }, [medicationFromStore]); useEffect(() => { // 同步剂量选择器和剂型选择器的默认值 if (medication) { setDosageValuePicker(medication.dosageValue); setDosageUnitPicker(medication.dosageUnit); setFormPicker(medication.form); } }, [medication?.dosageValue, medication?.dosageUnit, medication?.form]); useEffect(() => { setNoteDraft(medication?.note ?? ''); }, [medication?.note]); useEffect(() => { let isMounted = true; let abortController = new AbortController(); console.log('[MEDICATION_DETAIL] useEffect triggered', { medicationId, hasMedicationFromStore: !!medicationFromStore, deleteLoading }); // 如果正在删除操作中,不执行任何操作 if (deleteLoading) { console.log('[MEDICATION_DETAIL] Delete operation in progress, skipping useEffect'); return () => { isMounted = false; abortController.abort(); }; } if (!medicationId || medicationFromStore) { console.log('[MEDICATION_DETAIL] Early return from useEffect', { hasMedicationId: !!medicationId, hasMedicationFromStore: !!medicationFromStore }); return () => { isMounted = false; abortController.abort(); }; } console.log('[MEDICATION_DETAIL] Starting API call for medication', medicationId); setLoading(true); getMedicationById(medicationId) .then((data) => { if (!isMounted || abortController.signal.aborted) return; console.log('[MEDICATION_DETAIL] API call successful', data); setMedication(data); setError(null); // 如果服务端返回了 AI 分析结果,自动展示 if (data.aiAnalysis) { setAiAnalysisContent(data.aiAnalysis); } }) .catch((err) => { if (abortController.signal.aborted) return; console.error('加载药品详情失败', err); console.log('[MEDICATION_DETAIL] API call failed for medication', medicationId, err); if (isMounted) { setError(t('medications.detail.error.title')); } }) .finally(() => { if (isMounted && !abortController.signal.aborted) { setLoading(false); } }); return () => { isMounted = false; abortController.abort(); }; }, [medicationId, medicationFromStore, deleteLoading]); useEffect(() => { let isMounted = true; if (!medicationId) { return () => { isMounted = false; }; } setSummaryLoading(true); getMedicationRecords({ medicationId }) .then((records) => { if (!isMounted) return; const takenCount = records.filter((record) => record.status === 'taken').length; const earliestRecord = records.reduce((earliest, record) => { const current = new Date(record.scheduledTime); if (!earliest || current < earliest) { return current; } return earliest; }, null); const startedDaysRaw = earliestRecord ? dayjs().diff(dayjs(earliestRecord), 'day') : medication ? dayjs().diff(dayjs(medication.startDate), 'day') : null; const startedDays = typeof startedDaysRaw === 'number' ? Math.max(startedDaysRaw, 0) : null; setSummary({ takenCount, startedDays: startedDays ?? null, }); }) .catch((err) => { console.error('加载服药记录失败', err); }) .finally(() => { if (isMounted) { setSummaryLoading(false); } }); return () => { isMounted = false; }; }, [medicationId, medication]); const appendDictationResult = useCallback((text: string) => { const clean = text.trim(); if (!clean) return; setNoteDraft((prev) => { if (!prev) return clean; return `${prev}${prev.endsWith('\n') ? '' : '\n'}${clean}`; }); }, []); useEffect(() => { if (!isDictationSupported || !noteModalVisible) { return; } Voice.onSpeechStart = () => { setDictationActive(true); setDictationLoading(false); }; Voice.onSpeechEnd = () => { setDictationActive(false); setDictationLoading(false); }; Voice.onSpeechResults = (event: any) => { const recognized = event?.value?.[0]; if (recognized) { appendDictationResult(recognized); } }; Voice.onSpeechError = (error: any) => { console.log('[MEDICATION_DETAIL] voice error', error); setDictationActive(false); setDictationLoading(false); Alert.alert(t('medications.add.note.voiceError'), t('medications.add.note.voiceErrorMessage')); }; return () => { Voice.destroy() .then(() => { Voice.removeAllListeners(); }) .catch(() => {}); }; }, [appendDictationResult, isDictationSupported, noteModalVisible]); useEffect(() => { if (!noteModalVisible) { setKeyboardHeight(0); return; } const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'; const handleShow = (event: any) => { const height = event?.endCoordinates?.height ?? 0; setKeyboardHeight(height); }; const handleHide = () => setKeyboardHeight(0); const showSub = Keyboard.addListener(showEvent, handleShow); const hideSub = Keyboard.addListener(hideEvent, handleHide); return () => { showSub.remove(); hideSub.remove(); }; }, [noteModalVisible]); const handleDictationPress = useCallback(async () => { if (!isDictationSupported || dictationLoading) { return; } try { if (dictationActive) { setDictationLoading(true); await Voice.stop(); setDictationLoading(false); return; } setDictationLoading(true); try { await Voice.stop(); } catch { // ignore if not recording } await Voice.start('zh-CN'); } catch (error) { console.log('[MEDICATION_DETAIL] unable to start dictation', error); setDictationLoading(false); Alert.alert(t('medications.add.note.voiceStartError'), t('medications.add.note.voiceStartErrorMessage')); } }, [dictationActive, dictationLoading, isDictationSupported]); const closeNoteModal = useCallback(() => { setNoteModalVisible(false); if (dictationActive) { Voice.stop().catch(() => {}); } setDictationActive(false); setDictationLoading(false); setKeyboardHeight(0); }, [dictationActive]); const handleToggleMedication = async (nextValue: boolean) => { if (!medication || updatePending) return; // 如果是关闭激活状态,显示确认弹窗 if (!nextValue) { setDeactivateSheetVisible(true); return; } // 如果是开启激活状态,直接执行 try { setUpdatePending(true); const updated = await dispatch( updateMedicationAction({ id: medication.id, isActive: nextValue, }) ).unwrap(); setMedication(updated); // 重新安排药品通知 try { if (nextValue) { // 如果激活了药品,安排通知 await medicationNotificationService.scheduleMedicationNotifications(updated); } } catch (error) { console.error('[MEDICATION] 处理药品通知失败:', error); // 不影响药品状态切换的成功流程,只记录错误 } } catch (err) { console.error('切换药品状态失败', err); Alert.alert(t('medications.detail.toggleError.title'), t('medications.detail.toggleError.message')); } finally { setUpdatePending(false); } }; const handleDeactivateMedication = useCallback(async () => { if (!medication || deactivateLoading) return; try { setDeactivateLoading(true); setDeactivateSheetVisible(false); // 立即关闭确认对话框 const updated = await dispatch( updateMedicationAction({ id: medication.id, isActive: false, }) ).unwrap(); setMedication(updated); // 取消该药品的通知 try { await medicationNotificationService.cancelMedicationNotifications(updated.id); } catch (error) { console.error('[MEDICATION] 取消药品通知失败:', error); // 不影响药品状态切换的成功流程,只记录错误 } } catch (error) { console.error('停用药物失败', error); Alert.alert(t('medications.detail.deactivate.error.title'), t('medications.detail.deactivate.error.message')); } finally { setDeactivateLoading(false); } }, [dispatch, medication, deactivateLoading]); const formLabel = medication ? t(`medications.manage.formLabels.${medication.form}`) : ''; const dosageLabel = medication ? `${medication.dosageValue} ${medication.dosageUnit}` : '--'; const startDateLabel = medication ? dayjs(medication.startDate).format('YYYY年M月D日') : '--'; // 计算服药周期显示 const medicationPeriodLabel = useMemo(() => { if (!medication) return '--'; const startDate = dayjs(medication.startDate).format('YYYY年M月D日'); if (medication.endDate) { // 有结束日期,显示开始日期到结束日期 const endDate = dayjs(medication.endDate).format('YYYY年M月D日'); return `${startDate} - ${endDate}`; } else { // 没有结束日期,显示长期 return `${startDate} - ${t('medications.detail.plan.longTerm')}`; } }, [medication, t]); const reminderTimes = medication?.medicationTimes?.length ? medication.medicationTimes.join('、') : t('medications.manage.reminderNotSet'); const frequencyLabel = useMemo(() => { if (!medication) return '--'; switch (medication.repeatPattern) { case 'daily': return `${t('medications.manage.frequency.daily')} ${medication.timesPerDay} ${t('medications.add.frequency.value', { count: medication.timesPerDay }).split(' ')[1]}`; case 'weekly': return `${t('medications.manage.frequency.weekly')} ${medication.timesPerDay} ${t('medications.add.frequency.value', { count: medication.timesPerDay }).split(' ')[1]}`; default: return `${t('medications.manage.frequency.custom')} · ${medication.timesPerDay} ${t('medications.add.frequency.value', { count: medication.timesPerDay }).split(' ')[1]}`; } }, [medication, t]); const handleOpenNoteModal = useCallback(() => { setNoteDraft(medication?.note ?? ''); setNoteModalVisible(true); }, [medication?.note]); const handleSaveNote = useCallback(async () => { if (!medication) return; const trimmed = noteDraft.trim(); setNoteSaving(true); try { const updated = await dispatch( updateMedicationAction({ id: medication.id, note: trimmed || undefined, }) ).unwrap(); setMedication(updated); closeNoteModal(); } catch (err) { console.error('保存备注失败', err); Alert.alert(t('medications.detail.note.saveError.title'), t('medications.detail.note.saveError.message')); } finally { setNoteSaving(false); } }, [closeNoteModal, dispatch, medication, noteDraft]); const statusLabel = medication?.isActive ? t('medications.detail.status.enabled') : t('medications.detail.status.disabled'); const noteText = medication?.note?.trim() ? medication.note : t('medications.detail.note.noNote'); const dayStreakText = typeof summary.startedDays === 'number' ? t('medications.detail.overview.startedDays', { days: summary.startedDays }) : medication ? t('medications.detail.overview.startDate', { date: dayjs(medication.startDate).format('YYYY年M月D日') }) : t('medications.detail.overview.noStartDate'); const handleDeleteMedication = useCallback(async () => { if (!medication || deleteLoading) { console.log('[MEDICATION_DETAIL] Delete aborted', { hasMedication: !!medication, deleteLoading }); return; } console.log('[MEDICATION_DETAIL] Starting delete operation for medication', medication.id); try { setDeleteLoading(true); setDeleteSheetVisible(false); // 立即关闭确认对话框 // 先取消该药品的通知 try { await medicationNotificationService.cancelMedicationNotifications(medication.id); } catch (error) { console.error('[MEDICATION] 取消药品通知失败:', error); // 不影响药品删除的成功流程,只记录错误 } await dispatch(deleteMedicationAction(medication.id)).unwrap(); console.log('[MEDICATION_DETAIL] Delete operation successful, navigating back'); router.back(); } catch (err) { console.error('删除药品失败', err); Alert.alert(t('medications.detail.delete.error.title'), t('medications.detail.delete.error.message')); } finally { setDeleteLoading(false); } }, [deleteLoading, dispatch, medication, router]); const handleImagePreview = useCallback(() => { if (medication?.photoUrl) { setShowImagePreview(true); } }, [medication?.photoUrl]); const handleStartDatePress = useCallback(() => { if (!medication) return; const startDate = dayjs(medication.startDate).format('YYYY年M月D日'); let message; if (medication.endDate) { const endDate = dayjs(medication.endDate).format('YYYY年M月D日'); message = t('medications.detail.plan.periodMessage', { startDate, endDateInfo: t('medications.detail.plan.periodMessage', { endDate }) }); } else { message = t('medications.detail.plan.periodMessage', { startDate, endDateInfo: t('medications.detail.plan.longTermPlan') }); } Alert.alert(t('medications.detail.sections.plan'), message); }, [medication, t]); const handleTimePress = useCallback(() => { Alert.alert(t('medications.detail.plan.time'), t('medications.detail.plan.timeMessage', { times: reminderTimes })); }, [reminderTimes, t]); const handleDosagePress = useCallback(() => { if (!medication) return; setDosagePickerVisible(true); }, [medication]); const handleFormPress = useCallback(() => { if (!medication) return; setFormPickerVisible(true); }, [medication]); const handleFrequencyPress = useCallback(() => { if (!medication) return; // 跳转到独立的频率编辑页面 router.push({ pathname: ROUTES.MEDICATION_EDIT_FREQUENCY, params: { medicationId: medication.id, medicationName: medication.name, repeatPattern: medication.repeatPattern, timesPerDay: medication.timesPerDay.toString(), medicationTimes: medication.medicationTimes.join(','), }, }); }, [medication, router]); const confirmDosagePicker = useCallback(async () => { if (!medication || updatePending) return; setDosagePickerVisible(false); // 如果值没有变化,不需要更新 if (dosageValuePicker === medication.dosageValue && dosageUnitPicker === medication.dosageUnit) { return; } try { setUpdatePending(true); const updated = await dispatch( updateMedicationAction({ id: medication.id, dosageValue: dosageValuePicker, dosageUnit: dosageUnitPicker, }) ).unwrap(); setMedication(updated); // 重新安排药品通知 try { await medicationNotificationService.scheduleMedicationNotifications(updated); } catch (error) { console.error('[MEDICATION] 安排药品通知失败:', error); // 不影响药品更新的成功流程,只记录错误 } } catch (err) { console.error('更新剂量失败', err); Alert.alert(t('medications.detail.updateErrors.dosage'), t('medications.detail.updateErrors.dosageMessage')); } finally { setUpdatePending(false); } }, [dispatch, dosageUnitPicker, dosageValuePicker, medication, updatePending]); const confirmFormPicker = useCallback(async () => { if (!medication || updatePending) return; setFormPickerVisible(false); // 如果值没有变化,不需要更新 if (formPicker === medication.form) { return; } try { setUpdatePending(true); const updated = await dispatch( updateMedicationAction({ id: medication.id, form: formPicker, }) ).unwrap(); setMedication(updated); // 重新安排药品通知 try { await medicationNotificationService.scheduleMedicationNotifications(updated); } catch (error) { console.error('[MEDICATION] 安排药品通知失败:', error); // 不影响药品更新的成功流程,只记录错误 } } catch (err) { console.error('更新剂型失败', err); Alert.alert(t('medications.detail.updateErrors.form'), t('medications.detail.updateErrors.formMessage')); } finally { setUpdatePending(false); } }, [dispatch, formPicker, medication, updatePending]); // AI 分析处理函数 const handleAiAnalysis = useCallback(async () => { if (!medication || aiAnalysisLoading) return; // 1. 先验证用户是否登录 const isLoggedIn = await ensureLoggedIn(); if (!isLoggedIn) { return; // 如果未登录,ensureLoggedIn 会自动跳转到登录页 } // 2. 检查用户是否是 VIP 或有剩余免费次数 const serviceAccess = checkServiceAccess(); if (!serviceAccess.canUseService) { // 如果不能使用服务,弹出会员购买弹窗 openMembershipModal({ onPurchaseSuccess: () => { // 购买成功后自动执行 AI 分析 handleAiAnalysis(); }, }); return; } // 3. 通过验证,执行 AI 分析 // 重置状态 setAiAnalysisContent(''); setAiAnalysisLoading(true); // 滚动到底部,让用户看到分析内容 setTimeout(() => { scrollViewRef.current?.scrollToEnd({ animated: true }); }, 100); // 创建 AbortController 用于取消 const controller = new AbortController(); setAiAnalysisAbortController(controller); try { await analyzeMedicationStream( medication.id, { onChunk: (chunk: string) => { setAiAnalysisContent((prev) => prev + chunk); }, onEnd: () => { setAiAnalysisLoading(false); setAiAnalysisAbortController(null); // 重新加载药品详情以获取最新的 aiAnalysis 字段 if (medicationId) { getMedicationById(medicationId) .then((data) => { setMedication(data); }) .catch((err) => { console.error('重新加载药品详情失败', err); }); } }, onError: (error: any) => { console.error('[MEDICATION] AI 分析失败:', error); let errorMessage = t('medications.detail.aiAnalysis.error.message'); // 解析服务端返回的错误信息 if (error?.message) { if (error.message.includes('[ERROR]')) { errorMessage = error.message.replace('[ERROR]', '').trim(); } else if (error.message.includes('无权访问')) { errorMessage = t('medications.detail.aiAnalysis.error.forbidden'); } else if (error.message.includes('不存在')) { errorMessage = t('medications.detail.aiAnalysis.error.notFound'); } } else if (error?.status === 401) { errorMessage = t('medications.detail.aiAnalysis.error.unauthorized'); } else if (error?.status === 403) { errorMessage = t('medications.detail.aiAnalysis.error.forbidden'); } else if (error?.status === 404) { errorMessage = t('medications.detail.aiAnalysis.error.notFound'); } // 使用 Alert 弹窗显示错误 Alert.alert(t('medications.detail.aiAnalysis.error.title'), errorMessage); // 清空内容和加载状态 setAiAnalysisContent(''); setAiAnalysisLoading(false); setAiAnalysisAbortController(null); }, } ); } catch (error) { console.error('[MEDICATION] AI 分析异常:', error); // 使用 Alert 弹窗显示错误 Alert.alert(t('medications.detail.aiAnalysis.error.title'), t('medications.detail.aiAnalysis.error.networkError')); // 清空内容和加载状态 setAiAnalysisContent(''); setAiAnalysisLoading(false); setAiAnalysisAbortController(null); } }, [medication, aiAnalysisLoading, ensureLoggedIn, checkServiceAccess, openMembershipModal]); // 组件卸载时取消 AI 分析请求 useEffect(() => { return () => { if (aiAnalysisAbortController) { aiAnalysisAbortController.abort(); } }; }, [aiAnalysisAbortController]); if (!medicationId) { return ( {/* 背景渐变 */} {/* 装饰性圆圈 */} {t('medications.detail.notFound.title')} {t('medications.detail.notFound.subtitle')} ); } const isLoadingState = loading && !medication; const contentBottomPadding = Math.max(insets.bottom, 16) + 140; return ( {/* 背景渐变 */} {/* 装饰性圆圈 */} {isLoadingState ? ( {t('medications.detail.loading')} ) : error ? ( {error} {t('medications.detail.error.subtitle')} ) : medication ? ( {medication.photoUrl && ( )} {medication.name} {dosageLabel} · {formLabel}
{t('medications.detail.plan.frequency')} {frequencyLabel}
{t('medications.detail.note.label')} {noteText}
{summaryLoading ? t('medications.detail.overview.calculating') : t('medications.detail.overview.takenCount', { count: summary.takenCount })} {summaryLoading ? t('medications.detail.overview.calculatingDays') : dayStreakText}
{/* AI 分析结果展示 - 移动到底部 */} {(aiAnalysisContent || aiAnalysisLoading) && (
{aiAnalysisLoading && !aiAnalysisContent && ( {t('medications.detail.aiAnalysis.analyzing')} )} {aiAnalysisContent && ( {aiAnalysisContent} {aiAnalysisLoading && ( )} )}
)}
) : null} {medication ? ( {/* AI 分析按钮 - 占 2/3 宽度 */} {isLiquidGlassAvailable() ? ( {aiAnalysisLoading ? ( ) : ( )} {aiAnalysisLoading ? t('medications.detail.aiAnalysis.analyzingButton') : t('medications.detail.aiAnalysis.button')} ) : ( {aiAnalysisLoading ? ( ) : ( )} {aiAnalysisLoading ? t('medications.detail.aiAnalysis.analyzingButton') : t('medications.detail.aiAnalysis.button')} )} {/* 删除按钮 - 占 1/3 宽度 */} setDeleteSheetVisible(true)} > {isLiquidGlassAvailable() ? ( ) : ( )} ) : null} {t('medications.detail.note.edit')} {isDictationSupported && ( {dictationLoading ? ( ) : ( )} )} {!isDictationSupported && ( {t('medications.detail.note.voiceNotSupported')} )} {noteSaving ? ( ) : ( {t('medications.detail.note.save')} )} setDosagePickerVisible(false)} > setDosagePickerVisible(false)} /> {t('medications.detail.dosage.selectDosage')} {t('medications.detail.dosage.dosageValue')} setDosageValuePicker(Number(value))} itemStyle={styles.pickerItem} style={styles.picker} > {DOSAGE_VALUES.map((value) => ( ))} {t('medications.detail.dosage.unit')} setDosageUnitPicker(String(value))} itemStyle={styles.pickerItem} style={styles.picker} > {DOSAGE_UNITS.map((unit) => ( ))} setDosagePickerVisible(false)} style={[styles.pickerBtn, { borderColor: colors.border }]} > {t('medications.detail.pickers.cancel')} {t('medications.detail.pickers.confirm')} setFormPickerVisible(false)} > setFormPickerVisible(false)} /> {t('medications.detail.dosage.selectForm')} setFormPicker(value as MedicationForm)} itemStyle={styles.pickerItem} style={styles.picker} > {FORM_OPTIONS.map((option) => ( ))} setFormPickerVisible(false)} style={[styles.pickerBtn, { borderColor: colors.border }]} > {t('medications.detail.pickers.cancel')} {t('medications.detail.pickers.confirm')} {medication ? ( setDeleteSheetVisible(false)} onConfirm={handleDeleteMedication} title={t('medications.detail.delete.title', { name: medication.name })} description={t('medications.detail.delete.description')} confirmText={t('medications.detail.delete.confirm')} cancelText={t('medications.detail.delete.cancel')} destructive loading={deleteLoading} /> ) : null} {medication ? ( setDeactivateSheetVisible(false)} onConfirm={handleDeactivateMedication} title={t('medications.detail.deactivate.title', { name: medication.name })} description={t('medications.detail.deactivate.description')} confirmText={t('medications.detail.deactivate.confirm')} cancelText={t('medications.detail.deactivate.cancel')} destructive loading={deactivateLoading} /> ) : null} {/* 图片预览 */} {medication?.photoUrl && ( setShowImagePreview(false)} swipeToCloseEnabled={true} doubleTapToZoomEnabled={true} HeaderComponent={() => ( {medication.name} )} FooterComponent={() => ( setShowImagePreview(false)} > {t('medications.detail.imageViewer.close')} )} /> )}
); } const Section = ({ title, children, color, }: { title: string; children: React.ReactNode; color: string; }) => { return ( {title} {children} ); }; const styles = StyleSheet.create({ container: { flex: 1, position: 'relative', }, 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, }, content: { paddingHorizontal: 20, gap: 24, }, centered: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12, }, loadingText: { marginTop: 8, fontSize: 14, }, emptyTitle: { fontSize: 18, fontWeight: '600', textAlign: 'center', }, emptySubtitle: { fontSize: 14, textAlign: 'center', }, heroCard: { borderRadius: 28, padding: 20, flexDirection: 'row', justifyContent: 'space-between', backgroundColor: '#FFFFFF', alignItems: 'center', gap: 12, }, heroInfo: { flexDirection: 'row', alignItems: 'center', gap: 14, flex: 1, }, heroImageWrapper: { width: 64, height: 64, borderRadius: 20, backgroundColor: '#F2F2F2', alignItems: 'center', justifyContent: 'center', position: 'relative', }, heroImage: { width: '60%', height: '60%', borderRadius: '20%' }, heroTitle: { fontSize: 20, fontWeight: '700', }, heroMeta: { marginTop: 4, fontSize: 13, fontWeight: '500', }, heroToggle: { alignItems: 'flex-end', gap: 6, }, section: { gap: 12, }, sectionTitle: { fontSize: 18, fontWeight: '700', }, row: { flexDirection: 'row', gap: 12, }, fullCard: { borderRadius: 22, padding: 18, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, fullCardLeading: { flexDirection: 'row', alignItems: 'center', gap: 8, }, fullCardLabel: { fontSize: 15, fontWeight: '600', }, fullCardTrailing: { flexDirection: 'row', alignItems: 'center', gap: 6, }, fullCardValue: { fontSize: 16, fontWeight: '600', }, noteCard: { flexDirection: 'row', alignItems: 'center', gap: 14, borderRadius: 24, paddingHorizontal: 18, paddingVertical: 16 }, noteBody: { flex: 1, gap: 4, }, noteLabel: { fontSize: 14, fontWeight: '600', }, noteValue: { fontSize: 14, lineHeight: 20, }, summaryCard: { flexDirection: 'row', alignItems: 'center', borderRadius: 24, padding: 18, gap: 16, shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 12, shadowOffset: { width: 0, height: 3 }, elevation: 3, borderWidth: 1, borderColor: 'rgba(0, 0, 0, 0.04)', }, summaryIcon: { width: 44, height: 44, borderRadius: 22, backgroundColor: '#EEF1FF', alignItems: 'center', justifyContent: 'center', }, summaryBody: { flex: 1, gap: 4, }, summaryHighlight: { fontSize: 16, fontWeight: '700', }, summaryMeta: { fontSize: 14, }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.35)', justifyContent: 'flex-end', }, modalBackdrop: { flex: 1, }, modalContainer: { width: '100%', paddingHorizontal: 20, }, modalCard: { borderTopLeftRadius: 28, borderTopRightRadius: 28, paddingHorizontal: 20, paddingTop: 20, paddingBottom: 32, gap: 16, }, modalHandle: { width: 60, height: 5, borderRadius: 2.5, backgroundColor: 'rgba(0,0,0,0.12)', alignSelf: 'center', marginBottom: 12, }, modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, modalTitle: { fontSize: 18, fontWeight: '700', }, noteEditorWrapper: { borderWidth: 1, borderRadius: 24, paddingHorizontal: 18, paddingVertical: 24, minHeight: 120, paddingRight: 70, shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, elevation: 3, }, noteEditorInput: { minHeight: 50, fontSize: 15, lineHeight: 22, }, voiceButton: { position: 'absolute', right: 16, top: 16, width: 40, height: 40, borderRadius: 20, borderWidth: 1, alignItems: 'center', justifyContent: 'center', }, voiceHint: { fontSize: 12, }, modalActionGhostText: { fontSize: 16, fontWeight: '600', }, modalActionPrimary: { borderRadius: 22, height: 52, alignItems: 'center', justifyContent: 'center', marginHorizontal: 8, shadowOpacity: 0.25, shadowRadius: 12, shadowOffset: { width: 0, height: 8 }, elevation: 4, }, modalActionPrimaryText: { fontSize: 17, fontWeight: '700', }, modalActionContainer: { marginTop: 8, }, footerBar: { position: 'absolute', left: 0, right: 0, bottom: 0, paddingHorizontal: 20, paddingTop: 16, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: 'rgba(15,23,42,0.06)', }, deleteButton: { height: 56, borderRadius: 24, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, overflow: 'hidden', // 保证玻璃边界圆角效果 }, fallbackDeleteButton: { backgroundColor: '#EF4444', shadowColor: 'rgba(239,68,68,0.4)', shadowOffset: { width: 0, height: 10 }, shadowOpacity: 1, shadowRadius: 20, elevation: 6, }, // 底部按钮容器样式 footerButtonContainer: { flexDirection: 'row', gap: 12, }, aiAnalysisButtonWrapper: { flex: 2, // 占 2/3 宽度 }, deleteButtonWrapper: { flex: 1, // 占 1/3 宽度 }, aiAnalysisButton: { height: 56, borderRadius: 24, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, overflow: 'hidden', }, fallbackAiButton: { backgroundColor: '#3B82F6', shadowColor: 'rgba(59,130,246,0.4)', shadowOffset: { width: 0, height: 10 }, shadowOpacity: 1, shadowRadius: 20, elevation: 6, }, aiAnalysisButtonText: { fontSize: 17, fontWeight: '700', color: '#fff', }, // AI 分析卡片样式 aiAnalysisCard: { borderRadius: 24, padding: 20, minHeight: 100, shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 12, shadowOffset: { width: 0, height: 3 }, elevation: 3, borderWidth: 1, borderColor: 'rgba(0, 0, 0, 0.04)', }, aiAnalysisLoading: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingVertical: 12, }, aiAnalysisLoadingText: { fontSize: 15, }, aiAnalysisContentWrapper: { gap: 12, }, aiAnalysisText: { fontSize: 15, lineHeight: 24, }, aiAnalysisStreaming: { alignSelf: 'flex-start', marginTop: 8, }, deleteButtonText: { fontSize: 17, fontWeight: '700', color: '#fff', }, // Picker 相关样式 pickerBackdrop: { flex: 1, backgroundColor: 'rgba(15, 23, 42, 0.4)', }, pickerSheet: { position: 'absolute', left: 20, right: 20, bottom: 40, borderRadius: 24, padding: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 10 }, shadowOpacity: 0.2, shadowRadius: 20, elevation: 8, }, pickerTitle: { fontSize: 20, fontWeight: '700', marginBottom: 20, textAlign: 'center', }, pickerRow: { flexDirection: 'row', gap: 16, marginBottom: 20, }, pickerColumn: { flex: 1, gap: 8, }, pickerLabel: { fontSize: 14, fontWeight: '600', textAlign: 'center', }, picker: { width: '100%', height: 150, }, pickerItem: { fontSize: 18, height: 150, }, pickerActions: { flexDirection: 'row', gap: 12, marginTop: 16, }, pickerBtn: { flex: 1, paddingVertical: 14, borderRadius: 16, alignItems: 'center', borderWidth: 1, }, pickerBtnPrimary: { borderWidth: 0, }, pickerBtnText: { fontSize: 16, fontWeight: '600', }, imagePreviewHint: { position: 'absolute', top: 4, right: 4, backgroundColor: 'rgba(0, 0, 0, 0.5)', borderRadius: 10, padding: 4, }, // ImageViewing 组件样式 imageViewerHeader: { position: 'absolute', top: 60, left: 20, right: 20, backgroundColor: 'rgba(0, 0, 0, 0.7)', borderRadius: 12, paddingHorizontal: 16, paddingVertical: 12, zIndex: 1, }, imageViewerHeaderText: { color: '#FFF', fontSize: 14, fontWeight: '500', textAlign: 'center', }, imageViewerFooter: { position: 'absolute', bottom: 60, left: 20, right: 20, alignItems: 'center', zIndex: 1, }, imageViewerFooterButton: { backgroundColor: 'rgba(0, 0, 0, 0.7)', paddingHorizontal: 24, paddingVertical: 12, borderRadius: 20, }, imageViewerFooterButtonText: { color: '#FFF', fontSize: 16, fontWeight: '500', }, });