import { ThemedText } from '@/components/ThemedText'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { IconSymbol } from '@/components/ui/IconSymbol'; import { Colors } from '@/constants/Colors'; import { DOSAGE_UNITS, FORM_OPTIONS } from '@/constants/Medication'; import { useAppDispatch } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useCosUpload } from '@/hooks/useCosUpload'; import { createMedicationAction, fetchMedicationRecords, fetchMedications } from '@/store/medicationsSlice'; import type { MedicationForm, RepeatPattern } from '@/types/medication'; import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons'; import DateTimePicker from '@react-native-community/datetimepicker'; 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 * as ImagePicker from 'expo-image-picker'; import { LinearGradient } from 'expo-linear-gradient'; import { router } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, StyleSheet, TextInput, TouchableOpacity, View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; type ThemeColors = typeof Colors.light | typeof Colors.dark; const HEX_COLOR_REGEX = /^#([0-9a-f]{6})$/i; const withAlpha = (hex: string, alpha: number) => { if (!HEX_COLOR_REGEX.test(hex)) { return hex; } const normalized = hex.replace('#', ''); const r = parseInt(normalized.slice(0, 2), 16); const g = parseInt(normalized.slice(2, 4), 16); const b = parseInt(normalized.slice(4, 6), 16); const safeAlpha = Math.min(Math.max(alpha, 0), 1); return `rgba(${r}, ${g}, ${b}, ${safeAlpha})`; }; // 表单数据接口(用于内部状态管理) interface AddMedicationFormData { name: string; photoUrl?: string | null; form: MedicationForm; dosageValue: string; dosageUnit: string; timesPerDay: number; medicationTimes: string[]; startDate: string; note: string; } const TIMES_PER_DAY_OPTIONS = Array.from({ length: 10 }, (_, index) => index + 1); const STEP_TITLES = ['药品名称', '剂型与剂量', '服药频率', '服药时间', '备注']; const STEP_DESCRIPTIONS = [ '为药物命名并上传包装照片,方便识别', '选择药片类型并填写每次的用药剂量', '设置用药频率以及每日次数', '添加并管理每天的提醒时间', '填写备注或医生叮嘱(可选)', ]; const DEFAULT_TIME_PRESETS = ['08:00', '12:00', '18:00', '22:00']; const formatTime = (date: Date) => dayjs(date).format('HH:mm'); const getDefaultTimeByIndex = (index: number) => DEFAULT_TIME_PRESETS[index % DEFAULT_TIME_PRESETS.length]; const createDateFromTime = (time: string) => { try { if (!time || typeof time !== 'string') { console.warn('[MEDICATION] Invalid time string provided:', time); return new Date(); } const parts = time.split(':'); if (parts.length !== 2) { console.warn('[MEDICATION] Invalid time format:', time); return new Date(); } const hour = parseInt(parts[0], 10); const minute = parseInt(parts[1], 10); if (isNaN(hour) || isNaN(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59) { console.warn('[MEDICATION] Invalid time values:', { hour, minute }); return new Date(); } const next = new Date(); next.setHours(hour, minute, 0, 0); // 验证日期是否有效 if (isNaN(next.getTime())) { console.error('[MEDICATION] Failed to create valid date'); return new Date(); } return next; } catch (error) { console.error('[MEDICATION] Error in createDateFromTime:', error); return new Date(); } }; export default function AddMedicationScreen() { const dispatch = useAppDispatch(); const insets = useSafeAreaInsets(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colors: ThemeColors = Colors[theme]; const glassAvailable = isLiquidGlassAvailable(); const totalSteps = STEP_TITLES.length; const [currentStep, setCurrentStep] = useState(0); const [medicationName, setMedicationName] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const { upload, uploading } = useCosUpload({ prefix: 'images/medications' }); // 获取登录验证相关的功能 const { ensureLoggedIn } = useAuthGuard(); const softBorderColor = useMemo(() => withAlpha(colors.border, 0.25), [colors.border]); const fadedBorderFill = useMemo(() => withAlpha('#ffffff', 1), [colors.border]); const glassPrimaryTint = useMemo(() => withAlpha(colors.primary, theme === 'dark' ? 0.55 : 0.45), [colors.primary, theme]); const glassDisabledTint = useMemo(() => withAlpha(colors.border, theme === 'dark' ? 0.45 : 0.6), [colors.border, theme]); const glassPrimaryBackground = useMemo(() => withAlpha(colors.primary, theme === 'dark' ? 0.35 : 0.7), [colors.primary, theme]); const glassDisabledBackground = useMemo(() => withAlpha(colors.border, theme === 'dark' ? 0.35 : 0.5), [colors.border, theme]); const cardShadowColor = useMemo( () => (theme === 'dark' ? 'rgba(15, 23, 42, 0.45)' : 'rgba(15, 23, 42, 0.16)'), [theme] ); const [photoPreview, setPhotoPreview] = useState(null); const [photoUrl, setPhotoUrl] = useState(null); const [selectedForm, setSelectedForm] = useState('capsule'); const [dosageValue, setDosageValue] = useState('1'); const [dosageUnit, setDosageUnit] = useState(DOSAGE_UNITS[0]); const [unitPickerVisible, setUnitPickerVisible] = useState(false); const [unitPickerValue, setUnitPickerValue] = useState(DOSAGE_UNITS[0]); const [timesPerDay, setTimesPerDay] = useState(1); const [timesPickerVisible, setTimesPickerVisible] = useState(false); const [timesPickerValue, setTimesPickerValue] = useState(1); const [startDate, setStartDate] = useState(new Date()); const [endDate, setEndDate] = useState(null); const [expiryDate, setExpiryDate] = useState(null); const [datePickerVisible, setDatePickerVisible] = useState(false); const [datePickerValue, setDatePickerValue] = useState(new Date()); const [endDatePickerVisible, setEndDatePickerVisible] = useState(false); const [endDatePickerValue, setEndDatePickerValue] = useState(new Date()); const [expiryDatePickerVisible, setExpiryDatePickerVisible] = useState(false); const [expiryDatePickerValue, setExpiryDatePickerValue] = useState(new Date()); const [datePickerMode, setDatePickerMode] = useState<'start' | 'end'>('start'); const [medicationTimes, setMedicationTimes] = useState([DEFAULT_TIME_PRESETS[0]]); const [timePickerVisible, setTimePickerVisible] = useState(false); const [timePickerDate, setTimePickerDate] = useState(createDateFromTime(DEFAULT_TIME_PRESETS[0])); const [editingTimeIndex, setEditingTimeIndex] = useState(null); const [note, setNote] = useState(''); const [dictationActive, setDictationActive] = useState(false); const [dictationLoading, setDictationLoading] = useState(false); // 临时存储当前语音识别的结果,用于实时预览 const [currentDictationText, setCurrentDictationText] = useState(''); // 记录语音识别开始前的文本,用于取消时恢复 const [noteBeforeDictation, setNoteBeforeDictation] = useState(''); const isDictationSupported = Platform.OS === 'ios'; useEffect(() => { setMedicationTimes((prev) => { if (timesPerDay > prev.length) { const next = [...prev]; while (next.length < timesPerDay) { next.push(getDefaultTimeByIndex(next.length)); } return next; } if (timesPerDay < prev.length) { return prev.slice(0, timesPerDay); } return prev; }); }, [timesPerDay]); // 实时更新语音识别结果(替换式,不是追加) const updateDictationResult = useCallback( (text: string) => { const clean = text.trim(); if (!clean) return; // 实时更新:用识别的新文本替换当前识别文本 setCurrentDictationText(clean); // 同步更新到 note 中,以便用户能看到实时效果 setNote((prev) => { // 移除之前的语音识别文本,添加新的识别文本 const baseText = noteBeforeDictation; if (!baseText) { return clean; } // 在原文本后追加,确保格式正确 return `${baseText}${baseText.endsWith('\n') ? '' : '\n'}${clean}`; }); }, [noteBeforeDictation] ); // 确认语音识别结果 const confirmDictationResult = useCallback(() => { // 语音识别结束,确认当前文本 setCurrentDictationText(''); setNoteBeforeDictation(''); }, []); // 取消语音识别 const cancelDictationResult = useCallback(() => { // 恢复到语音识别前的文本 setNote(noteBeforeDictation); setCurrentDictationText(''); setNoteBeforeDictation(''); }, [noteBeforeDictation]); const stepTitle = STEP_TITLES[currentStep] ?? STEP_TITLES[0]; const stepDescription = STEP_DESCRIPTIONS[currentStep] ?? ''; const canProceed = useMemo(() => { switch (currentStep) { case 0: return medicationName.trim().length > 0; case 1: return Number(dosageValue) > 0 && !!dosageUnit && !!selectedForm; case 2: return timesPerDay > 0; case 3: return medicationTimes.length > 0; default: return true; } }, [currentStep, dosageUnit, dosageValue, medicationName, medicationTimes.length, selectedForm, timesPerDay]); useEffect(() => { if (!isDictationSupported) { return; } Voice.onSpeechStart = () => { setDictationActive(true); setDictationLoading(false); }; Voice.onSpeechEnd = () => { // 语音识别结束,确认识别结果 confirmDictationResult(); setDictationActive(false); setDictationLoading(false); }; Voice.onSpeechResults = (event: any) => { // 获取最新的识别结果(这是累积的结果,包含之前说过的内容) const recognized = event?.value?.[0]; if (recognized) { // 实时更新识别结果,替换式而非追加式 updateDictationResult(recognized); } }; Voice.onSpeechError = (error: any) => { console.log('[MEDICATION] voice error', error); // 发生错误时取消识别 cancelDictationResult(); setDictationActive(false); setDictationLoading(false); Alert.alert('语音识别不可用', '无法使用语音输入,请检查权限设置后重试'); }; return () => { Voice.destroy() .then(() => { Voice.removeAllListeners(); }) .catch(() => {}); }; }, [updateDictationResult, confirmDictationResult, cancelDictationResult, isDictationSupported]); const handleNext = useCallback(async () => { if (!canProceed) return; // 如果不是最后一步,继续下一步 if (currentStep < totalSteps - 1) { setCurrentStep((prev) => Math.min(prev + 1, totalSteps - 1)); return; } // 最后一步:提交药物数据 setIsSubmitting(true); try { // 先检查用户是否已登录,如果未登录则跳转到登录页面 const isLoggedIn = await ensureLoggedIn({ shouldBack: true }); if (!isLoggedIn) { // 未登录,ensureLoggedIn 已处理跳转,直接返回 setIsSubmitting(false); return; } // 构建药物数据,符合 CreateMedicationDto 接口 const medicationData = { name: medicationName.trim(), photoUrl: photoUrl || undefined, form: selectedForm, dosageValue: Number(dosageValue), dosageUnit: dosageUnit, timesPerDay: timesPerDay, medicationTimes: medicationTimes, startDate: dayjs(startDate).startOf('day').toISOString(), // ISO 8601 格式 endDate: endDate ? dayjs(endDate).endOf('day').toISOString() : undefined, // 如果有结束日期,设置为当天结束时间 expiryDate: expiryDate ? dayjs(expiryDate).endOf('day').toISOString() : undefined, // 如果有有效期,设置为当天结束时间 repeatPattern: 'daily' as RepeatPattern, note: note.trim() || undefined, }; // 调用 Redux action 创建药物 const result = await dispatch(createMedicationAction(medicationData)).unwrap(); // 刷新药物列表和记录数据,确保返回主界面能看到新数据 await dispatch(fetchMedications({ isActive: true })); // 获取今天的记录,确保新添加的药物记录能显示 const today = dayjs().format('YYYY-MM-DD'); await dispatch(fetchMedicationRecords({ date: today })); // 成功提示 Alert.alert( '添加成功', `已成功添加药物"${medicationName}"`, [ { text: '确定', onPress: () => router.back(), }, ] ); } catch (error) { console.error('[MEDICATION] 创建药物失败', error); Alert.alert( '添加失败', error instanceof Error ? error.message : '创建药物时发生错误,请稍后重试', [{ text: '确定' }] ); } finally { setIsSubmitting(false); } }, [ canProceed, currentStep, totalSteps, medicationName, photoUrl, selectedForm, dosageValue, dosageUnit, medicationTimes, startDate, endDate, note, dispatch, ensureLoggedIn, ]); const handlePrev = useCallback(() => { if (currentStep === 0) return; setCurrentStep((prev) => Math.max(prev - 1, 0)); }, [currentStep]); const handleDictationPress = useCallback(async () => { if (!isDictationSupported || dictationLoading) { return; } try { if (dictationActive) { // 停止录音 setDictationLoading(true); await Voice.stop(); // Voice.onSpeechEnd 会自动确认结果 setDictationLoading(false); return; } // 开始录音前,保存当前的文本内容 setNoteBeforeDictation(note); setCurrentDictationText(''); setDictationLoading(true); try { // 确保之前的录音已停止 await Voice.stop(); } catch { // 忽略错误:如果之前没有录音,stop 会抛出异常 } // 开始语音识别 await Voice.start('zh-CN'); } catch (error) { console.log('[MEDICATION] unable to start dictation', error); setDictationLoading(false); Alert.alert('无法启动语音输入', '请检查麦克风与语音识别权限后重试'); } }, [dictationActive, dictationLoading, isDictationSupported, note]); // 处理图片选择(拍照或相册) const handleSelectPhoto = useCallback(() => { Alert.alert( '选择图片', '请选择图片来源', [ { text: '拍照', onPress: async () => { try { const permission = await ImagePicker.requestCameraPermissionsAsync(); if (permission.status !== 'granted') { Alert.alert('权限不足', '需要相机权限以拍摄药品照片'); return; } const result = await ImagePicker.launchCameraAsync({ allowsEditing: true, quality: 0.3, aspect: [9,16] }); if (result.canceled || !result.assets?.length) { return; } const asset = result.assets[0]; setPhotoPreview(asset.uri); setPhotoUrl(null); try { const { url } = await upload( { uri: asset.uri, name: asset.fileName ?? `medication-${Date.now()}.jpg`, type: asset.mimeType ?? 'image/jpeg' }, { prefix: 'images/medications' } ); setPhotoUrl(url); } catch (uploadError) { console.error('[MEDICATION] 图片上传失败', uploadError); Alert.alert('上传失败', '图片上传失败,请稍后重试'); } } catch (error) { console.error('[MEDICATION] 拍照失败', error); Alert.alert('拍照失败', '无法打开相机,请稍后再试'); } }, }, { text: '从相册选择', onPress: async () => { try { const permission = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (permission.status !== 'granted') { Alert.alert('权限不足', '需要相册权限以选择药品照片'); return; } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], quality: 0.9, }); if (result.canceled || !result.assets?.length) { return; } const asset = result.assets[0]; setPhotoPreview(asset.uri); setPhotoUrl(null); try { const { url } = await upload( { uri: asset.uri, name: asset.fileName ?? `medication-${Date.now()}.jpg`, type: asset.mimeType ?? 'image/jpeg' }, { prefix: 'images/medications' } ); setPhotoUrl(url); } catch (uploadError) { console.error('[MEDICATION] 图片上传失败', uploadError); Alert.alert('上传失败', '图片上传失败,请稍后重试'); } } catch (error) { console.error('[MEDICATION] 从相册选择失败', error); Alert.alert('选择失败', '无法打开相册,请稍后再试'); } }, }, { text: '取消', style: 'cancel', }, ], { cancelable: true } ); }, [upload]); const handleRemovePhoto = useCallback(() => { setPhotoPreview(null); setPhotoUrl(null); }, []); const openStartDatePicker = useCallback(() => { setDatePickerValue(startDate); setDatePickerVisible(true); }, [startDate]); const openEndDatePicker = useCallback(() => { setEndDatePickerValue(endDate || new Date()); setEndDatePickerVisible(true); }, [endDate]); const openExpiryDatePicker = useCallback(() => { setExpiryDatePickerValue(expiryDate || new Date()); setExpiryDatePickerVisible(true); }, [expiryDate]); const confirmStartDate = useCallback((date: Date) => { // 验证开始日期不能早于今天 const today = new Date(); today.setHours(0, 0, 0, 0); const selectedDate = new Date(date); selectedDate.setHours(0, 0, 0, 0); if (selectedDate < today) { Alert.alert('日期无效', '开始日期不能早于今天'); return; } setStartDate(date); setDatePickerVisible(false); // 如果结束日期早于新的开始日期,清空结束日期 if (endDate && endDate < date) { setEndDate(null); } }, [endDate]); const confirmEndDate = useCallback((date: Date) => { // 验证结束日期不能早于开始日期 if (date < startDate) { Alert.alert('日期无效', '结束日期不能早于开始日期'); return; } setEndDate(date); setEndDatePickerVisible(false); }, [startDate]); const confirmExpiryDate = useCallback((date: Date) => { // 验证有效期不能早于今天 const today = new Date(); today.setHours(0, 0, 0, 0); const selectedDate = new Date(date); selectedDate.setHours(0, 0, 0, 0); if (selectedDate < today) { Alert.alert('日期无效', '有效期不能早于今天'); return; } setExpiryDate(date); setExpiryDatePickerVisible(false); }, []); const openTimePicker = useCallback( (index?: number) => { try { if (typeof index === 'number') { if (index >= 0 && index < medicationTimes.length) { setEditingTimeIndex(index); setTimePickerDate(createDateFromTime(medicationTimes[index])); } else { console.error('[MEDICATION] Invalid time index:', index); return; } } else { setEditingTimeIndex(null); setTimePickerDate(createDateFromTime(getDefaultTimeByIndex(medicationTimes.length))); } setTimePickerVisible(true); } catch (error) { console.error('[MEDICATION] Error in openTimePicker:', error); } }, [medicationTimes] ); const confirmTime = useCallback( (date: Date) => { try { if (!date || isNaN(date.getTime())) { console.error('[MEDICATION] Invalid date provided to confirmTime'); setTimePickerVisible(false); setEditingTimeIndex(null); return; } const nextValue = formatTime(date); setMedicationTimes((prev) => { if (editingTimeIndex == null) { return [...prev, nextValue]; } return prev.map((time, idx) => (idx === editingTimeIndex ? nextValue : time)); }); setTimePickerVisible(false); setEditingTimeIndex(null); } catch (error) { console.error('[MEDICATION] Error in confirmTime:', error); setTimePickerVisible(false); setEditingTimeIndex(null); } }, [editingTimeIndex] ); const removeTime = useCallback((index: number) => { setMedicationTimes((prev) => { if (prev.length === 1) { return prev; } return prev.filter((_, idx) => idx !== index); }); }, []); useEffect(() => { setUnitPickerVisible(false); setTimesPickerVisible(false); }, [currentStep]); const openUnitPicker = useCallback(() => { setUnitPickerValue(dosageUnit); setUnitPickerVisible(true); }, [dosageUnit]); const closeUnitPicker = useCallback(() => { setUnitPickerVisible(false); }, []); const confirmUnitPicker = useCallback(() => { setDosageUnit(unitPickerValue); setUnitPickerVisible(false); }, [unitPickerValue]); const openTimesPicker = useCallback(() => { setTimesPickerValue(timesPerDay); setTimesPickerVisible(true); }, [timesPerDay]); const closeTimesPicker = useCallback(() => { setTimesPickerVisible(false); }, []); const confirmTimesPicker = useCallback(() => { setTimesPerDay(timesPickerValue); setTimesPickerVisible(false); }, [timesPickerValue]); const renderStepContent = () => { switch (currentStep) { case 0: return ( {photoPreview ? ( <> {uploading ? '上传中…' : '重新选择'} ) : ( 上传药品图片 拍照或从相册选择,辅助识别药品包装 )} {uploading && ( 正在上传 )} ); case 1: return ( 每次剂量 {dosageUnit} 类型 {FORM_OPTIONS.map((option) => { const active = selectedForm === option.id; return ( setSelectedForm(option.id)} activeOpacity={0.9} > {option.label} ); })} ); case 2: return ( 每日次数 {`${timesPerDay} 次/日`} 用药周期 开始 {dayjs(startDate).format('MM/DD')} 结束 {endDate ? dayjs(endDate).format('MM/DD') : '长期'} 药品有效期 有效期至 {expiryDate ? dayjs(expiryDate).format('YYYY/MM/DD') : '未设置'} ); case 3: return ( 每日提醒时间 {medicationTimes.map((time, index) => ( openTimePicker(index)}> {time} removeTime(index)} disabled={medicationTimes.length === 1} hitSlop={12}> ))} openTimePicker()}> 添加时间 ); case 4: return ( 备注 {isDictationSupported && ( {dictationLoading ? ( ) : ( )} )} ); default: return null; } }; const showDateField = currentStep === 2; return ( {/* 背景渐变 */} {/* 装饰性圆圈 */} router.back()} withSafeTop={false} transparent variant="elevated" /> {Array.from({ length: totalSteps }).map((_, index) => { const isActive = index <= currentStep; return ( ); })} {stepTitle} {stepDescription} {renderStepContent()} {currentStep > 0 && ( 上一步 )} {glassAvailable ? ( {isSubmitting ? ( ) : ( {currentStep === totalSteps - 1 ? '完成' : '下一步'} )} ) : ( {isSubmitting ? ( ) : ( {currentStep === totalSteps - 1 ? '完成' : '下一步'} )} )} setDatePickerVisible(false)} > setDatePickerVisible(false)} /> 选择开始日期 { if (Platform.OS === 'ios') { if (date) setDatePickerValue(date); } else { if (event.type === 'set' && date) { confirmStartDate(date); } else { setDatePickerVisible(false); } } }} /> {Platform.OS === 'ios' && ( setDatePickerVisible(false)} style={[styles.modalBtn, { borderColor: softBorderColor }]} > 取消 confirmStartDate(datePickerValue)} style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colors.primary }]} > 确定 )} setExpiryDatePickerVisible(false)} > setExpiryDatePickerVisible(false)} /> 选择药品有效期 { if (Platform.OS === 'ios') { if (date) setExpiryDatePickerValue(date); } else { if (event.type === 'set' && date) { confirmExpiryDate(date); } else { setExpiryDatePickerVisible(false); } } }} /> {Platform.OS === 'ios' && ( setExpiryDatePickerVisible(false)} style={[styles.modalBtn, { borderColor: softBorderColor }]} > 取消 confirmExpiryDate(expiryDatePickerValue)} style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colors.primary }]} > 确定 )} setEndDatePickerVisible(false)} > setEndDatePickerVisible(false)} /> 选择结束日期 { if (Platform.OS === 'ios') { if (date) setEndDatePickerValue(date); } else { if (event.type === 'set' && date) { confirmEndDate(date); } else { setEndDatePickerVisible(false); } } }} /> {Platform.OS === 'ios' && ( setEndDatePickerVisible(false)} style={[styles.modalBtn, { borderColor: softBorderColor }]} > 取消 confirmEndDate(endDatePickerValue)} style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colors.primary }]} > 确定 )} { setTimePickerVisible(false); setEditingTimeIndex(null); }} > { setTimePickerVisible(false); setEditingTimeIndex(null); }} /> {editingTimeIndex !== null ? '修改提醒时间' : '添加提醒时间'} { if (Platform.OS === 'ios') { if (date) setTimePickerDate(date); } else { if (event.type === 'set' && date) { confirmTime(date); } else { setTimePickerVisible(false); setEditingTimeIndex(null); } } }} /> {Platform.OS === 'ios' && ( { setTimePickerVisible(false); setEditingTimeIndex(null); }} style={[styles.modalBtn, { borderColor: softBorderColor }]} > 取消 confirmTime(timePickerDate)} style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colors.primary }]} > 确定 )} 选择每日次数 setTimesPickerValue(Number(value))} itemStyle={styles.unitPickerItem} style={styles.unitPicker} > {TIMES_PER_DAY_OPTIONS.map((count) => ( ))} 取消 确定 选择剂量单位 setUnitPickerValue(String(value))} itemStyle={styles.unitPickerItem} style={styles.unitPicker} > {DOSAGE_UNITS.map((unit) => ( ))} 取消 确定 ); } const styles = StyleSheet.create({ screen: { 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, }, flex: { flex: 1, }, pageContent: { gap: 24, }, formSurface: { paddingHorizontal: 24, gap: 20, width: '100%', }, stepIndicator: { flexDirection: 'row', gap: 12, alignItems: 'center', }, stepSegment: { flex: 1, height: 4, borderRadius: 2, }, titleBlock: { gap: 6, }, modalTitle: { fontSize: 22, fontWeight: '600', }, modalSubtitle: { fontSize: 14, lineHeight: 20, }, contentContainer: { paddingBottom: 16, gap: 20, }, stepSection: { gap: 20, }, searchField: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, borderRadius: 16, height: 56, gap: 12, }, inputShadow: { borderWidth: 0, shadowColor: 'rgba(15, 23, 42, 0.16)', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.12, shadowRadius: 14, elevation: 6, }, searchInput: { flex: 1, fontSize: 16, paddingVertical: 0, }, photoCard: { borderWidth: 1, borderRadius: 20, height: 240, overflow: 'hidden', justifyContent: 'center', }, photoPlaceholder: { alignItems: 'center', justifyContent: 'center', gap: 10, paddingHorizontal: 20, }, photoIconBadge: { width: 48, height: 48, borderRadius: 24, alignItems: 'center', justifyContent: 'center', }, photoTitle: { fontSize: 16, fontWeight: '600', }, photoSubtitle: { fontSize: 13, textAlign: 'center', }, photoPreview: { width: '100%', height: '100%', }, photoOverlay: { position: 'absolute', right: 16, top: 16, flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 12, paddingVertical: 8, backgroundColor: 'rgba(15, 23, 42, 0.55)', borderRadius: 999, }, photoOverlayText: { color: '#fff', fontSize: 13, fontWeight: '600', }, photoUploadingIndicator: { position: 'absolute', bottom: 16, left: 16, flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 12, paddingVertical: 8, backgroundColor: 'rgba(15, 23, 42, 0.6)', borderRadius: 999, }, uploadingText: { fontSize: 12, fontWeight: '600', }, photoRemoveBtn: { position: 'absolute', right: 12, bottom: 12, width: 28, height: 28, borderRadius: 14, backgroundColor: 'rgba(255,255,255,0.85)', alignItems: 'center', justifyContent: 'center', }, inputGroup: { gap: 12, }, groupLabel: { fontSize: 14, fontWeight: '600', letterSpacing: 0.2, }, dosageField: { borderRadius: 16, paddingHorizontal: 16, paddingVertical: 12, position: 'relative', overflow: 'visible', }, dosageInput: { fontSize: 28, fontWeight: '600', flex: 1, paddingVertical: 0, }, dosageRow: { flexDirection: 'row', alignItems: 'center', gap: 12, }, unitSelector: { flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 12, }, unitSelectorText: { fontSize: 14, fontWeight: '600', }, unitPicker: { width: '100%', }, unitPickerItem: { fontSize: 16, }, formGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, }, formOption: { flexBasis: '30%', borderWidth: 1, borderRadius: 16, paddingVertical: 6, alignItems: 'center', justifyContent: 'center', }, formIconBadge: { width: 28, height: 28, borderRadius: 16, alignItems: 'center', justifyContent: 'center', }, formLabel: { fontSize: 11, fontWeight: '600', }, frequencyRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', borderWidth: 1, borderRadius: 16, paddingHorizontal: 16, paddingVertical: 14, }, frequencyValue: { fontSize: 18, fontWeight: '600', }, stepperBtn: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', }, frequencyLabel: { fontSize: 16, fontWeight: '600', }, unitSwitch: { flexDirection: 'row', gap: 12, marginTop: 10, }, unitOption: { flex: 1, paddingVertical: 12, borderWidth: 1, borderRadius: 14, alignItems: 'center', }, timeList: { gap: 12, }, timeItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', borderWidth: 1, borderRadius: 16, paddingHorizontal: 16, paddingVertical: 12, }, timeValue: { flexDirection: 'row', alignItems: 'center', gap: 8, }, timeText: { fontSize: 18, fontWeight: '600', }, addTimeButton: { borderWidth: 1, borderRadius: 16, paddingVertical: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, }, addTimeLabel: { fontSize: 14, fontWeight: '600', }, noteInput: { borderRadius: 20, padding: 16, paddingRight: 72, paddingBottom: 56, minHeight: 140, textAlignVertical: 'top', fontSize: 15, lineHeight: 22, }, noteInputWrapper: { position: 'relative', borderRadius: 24, }, noteVoiceButton: { position: 'absolute', right: 16, bottom: 16, width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', borderWidth: 1, }, footer: { gap: 12, }, periodHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, dateRowContainer: { flexDirection: 'row', gap: 12, }, dateRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', borderWidth: 1, borderRadius: 18, paddingHorizontal: 12, paddingVertical: 10, }, dateRowHalf: { flex: 1, }, dateLeft: { flexDirection: 'row', alignItems: 'center', gap: 8, }, dateLabel: { fontSize: 11, }, dateValue: { fontSize: 14, fontWeight: '600', }, startDateRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', borderWidth: 1, borderRadius: 18, paddingHorizontal: 14, paddingVertical: 12, }, startDateLeft: { flexDirection: 'row', alignItems: 'center', gap: 12, }, startDateLabel: { fontSize: 12, }, startDateValue: { fontSize: 16, fontWeight: '600', }, footerButtons: { flexDirection: 'row', gap: 48, alignItems: 'center', }, secondaryBtn: { paddingVertical: 14, paddingHorizontal: 20, backgroundColor: '#F2F2F2' }, secondaryBtnText: { fontSize: 15, fontWeight: '600', color: '#475569', }, primaryBtn: { flex: 1, paddingVertical: 16, borderRadius: 10, alignItems: 'center', justifyContent: 'center', overflow: 'hidden', }, primaryBtnWrapper: { flex: 1, }, primaryBtnWrapperDisabled: { opacity: 0.6, }, glassPrimaryBtn: { borderWidth: 1, borderColor: 'rgba(255,255,255,0.2)', }, primaryBtnFallback: { justifyContent: 'center', }, primaryBtnText: { fontSize: 16, fontWeight: '700', }, pickerBackdrop: { flex: 1, backgroundColor: 'rgba(15, 23, 42, 0.4)', }, pickerSheet: { position: 'absolute', left: 20, right: 20, bottom: 40, borderRadius: 24, padding: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 10 }, shadowOpacity: 0.2, shadowRadius: 20, elevation: 8, }, modalActions: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, marginTop: 16, }, modalBtn: { flex: 1, borderRadius: 16, paddingVertical: 12, alignItems: 'center', borderWidth: 1, borderColor: '#E2E8F0', }, modalBtnPrimary: { backgroundColor: '#4F46E5', borderColor: 'transparent', }, modalBtnText: { fontSize: 15, fontWeight: '600', color: '#475569', }, modalBtnTextPrimary: { color: '#fff', }, });