From 7ea558847d7b278c2d05b615e74dd39e2825eb52 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Tue, 11 Nov 2025 11:31:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(medications):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E8=8D=AF=E5=93=81=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2=E7=9A=84?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加剂量、剂型和服药频率的交互式选择器 - 实现提醒时间的动态编辑和添加功能 - 引入玻璃效果优化删除按钮的视觉体验 - 重构常量配置,提取药物相关常量到独立文件 - 创建可复用的InfoCard组件支持玻璃效果 --- app/medications/[medicationId].tsx | 812 +++++++++++++++++++++++++---- app/medications/add-medication.tsx | 12 +- components/ui/InfoCard.tsx | 149 ++++++ constants/Medication.ts | 56 ++ 4 files changed, 922 insertions(+), 107 deletions(-) create mode 100644 components/ui/InfoCard.tsx create mode 100644 constants/Medication.ts diff --git a/app/medications/[medicationId].tsx b/app/medications/[medicationId].tsx index e504210..423af81 100644 --- a/app/medications/[medicationId].tsx +++ b/app/medications/[medicationId].tsx @@ -1,7 +1,9 @@ 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_LABELS, FORM_OPTIONS, TIMES_PER_DAY_OPTIONS } from '@/constants/Medication'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; import { getMedicationById, getMedicationRecords } from '@/services/medications'; @@ -11,10 +13,13 @@ import { selectMedications, updateMedicationAction, } from '@/store/medicationsSlice'; -import type { Medication } from '@/types/medication'; +import type { Medication, MedicationForm, RepeatPattern } from '@/types/medication'; import { Ionicons } 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 { useLocalSearchParams, useRouter } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; @@ -24,6 +29,7 @@ import { Keyboard, Modal, Platform, + Pressable, ScrollView, StyleSheet, Switch, @@ -34,16 +40,6 @@ import { } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -const FORM_LABELS: Record = { - capsule: '胶囊', - pill: '药片', - injection: '注射', - spray: '喷雾', - drop: '滴剂', - syrup: '糖浆', - other: '其他', -}; - const DEFAULT_IMAGE = require('@/assets/images/medicine/image-medicine.png'); type RecordsSummary = { @@ -83,6 +79,84 @@ export default function MedicationDetailScreen() { const [keyboardHeight, setKeyboardHeight] = useState(0); const [deleteSheetVisible, setDeleteSheetVisible] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); + + // 剂量选择相关状态 + 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' + ); + + // 频率选择相关状态 + const [frequencyPickerVisible, setFrequencyPickerVisible] = useState(false); + const [repeatPatternPicker, setRepeatPatternPicker] = useState( + medicationFromStore?.repeatPattern ?? 'daily' + ); + const [timesPerDayPicker, setTimesPerDayPicker] = useState( + medicationFromStore?.timesPerDay ?? 1 + ); + + // 提醒时间相关状态 + const [timePickerVisible, setTimePickerVisible] = useState(false); + const [timePickerDate, setTimePickerDate] = useState(new Date()); + const [editingTimeIndex, setEditingTimeIndex] = useState(null); + const [medicationTimesPicker, setMedicationTimesPicker] = useState( + medicationFromStore?.medicationTimes ?? [] + ); + + // 辅助函数:从时间字符串创建 Date 对象 + const createDateFromTime = useCallback((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(); + } + }, []); + + // 辅助函数:格式化时间 + const formatTime = useCallback((date: Date) => dayjs(date).format('HH:mm'), []); + + // 辅助函数:获取默认时间 + const getDefaultTimeByIndex = useCallback((index: number) => { + const DEFAULT_TIME_PRESETS = ['08:00', '12:00', '18:00', '22:00']; + return DEFAULT_TIME_PRESETS[index % DEFAULT_TIME_PRESETS.length]; + }, []); useEffect(() => { if (!medicationFromStore) { @@ -97,6 +171,37 @@ export default function MedicationDetailScreen() { } }, [medicationFromStore]); + useEffect(() => { + // 同步剂量选择器、剂型选择器、频率选择器和时间选择器的默认值 + if (medication) { + setDosageValuePicker(medication.dosageValue); + setDosageUnitPicker(medication.dosageUnit); + setFormPicker(medication.form); + setRepeatPatternPicker(medication.repeatPattern); + setTimesPerDayPicker(medication.timesPerDay); + setMedicationTimesPicker(medication.medicationTimes || []); + } + }, [medication?.dosageValue, medication?.dosageUnit, medication?.form, medication?.repeatPattern, medication?.timesPerDay, medication?.medicationTimes]); + + // 根据 timesPerDayPicker 动态调整 medicationTimesPicker(与 add-medication.tsx 逻辑一致) + useEffect(() => { + setMedicationTimesPicker((prev) => { + if (timesPerDayPicker > prev.length) { + // 需要添加更多时间 + const next = [...prev]; + while (next.length < timesPerDayPicker) { + next.push(getDefaultTimeByIndex(next.length)); + } + return next; + } + if (timesPerDayPicker < prev.length) { + // 需要删除多余时间 + return prev.slice(0, timesPerDayPicker); + } + return prev; + }); + }, [timesPerDayPicker, getDefaultTimeByIndex]); + useEffect(() => { setNoteDraft(medication?.note ?? ''); }, [medication?.note]); @@ -422,12 +527,178 @@ export default function MedicationDetailScreen() { }, [reminderTimes]); const handleDosagePress = useCallback(() => { - Alert.alert('每次剂量', `单次服用剂量:${dosageLabel}`); - }, [dosageLabel]); + if (!medication) return; + setDosagePickerVisible(true); + }, [medication]); const handleFormPress = useCallback(() => { - Alert.alert('剂型', `药品剂型:${formLabel}`); - }, [formLabel]); + if (!medication) return; + setFormPickerVisible(true); + }, [medication]); + + const handleFrequencyPress = useCallback(() => { + if (!medication) return; + setFrequencyPickerVisible(true); + }, [medication]); + + 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); + } catch (err) { + console.error('更新剂量失败', err); + Alert.alert('更新失败', '更新剂量时出现问题,请稍后重试。'); + } 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); + } catch (err) { + console.error('更新剂型失败', err); + Alert.alert('更新失败', '更新剂型时出现问题,请稍后重试。'); + } finally { + setUpdatePending(false); + } + }, [dispatch, formPicker, medication, updatePending]); + + const confirmFrequencyPicker = useCallback(async () => { + if (!medication || updatePending) return; + + setFrequencyPickerVisible(false); + + // 检查频率和时间是否都没有变化 + const frequencyChanged = repeatPatternPicker !== medication.repeatPattern || timesPerDayPicker !== medication.timesPerDay; + const timesChanged = JSON.stringify(medicationTimesPicker) !== JSON.stringify(medication.medicationTimes); + + if (!frequencyChanged && !timesChanged) { + return; + } + + try { + setUpdatePending(true); + const updated = await dispatch( + updateMedicationAction({ + id: medication.id, + repeatPattern: repeatPatternPicker, + timesPerDay: timesPerDayPicker, + medicationTimes: medicationTimesPicker, // 同时更新提醒时间 + }) + ).unwrap(); + setMedication(updated); + } catch (err) { + console.error('更新频率失败', err); + Alert.alert('更新失败', '更新服药频率时出现问题,请稍后重试。'); + } finally { + setUpdatePending(false); + } + }, [dispatch, repeatPatternPicker, timesPerDayPicker, medicationTimesPicker, medication, updatePending]); + + // 打开时间选择器 + const openTimePicker = useCallback( + (index?: number) => { + try { + if (typeof index === 'number') { + if (index >= 0 && index < medicationTimesPicker.length) { + setEditingTimeIndex(index); + setTimePickerDate(createDateFromTime(medicationTimesPicker[index])); + } else { + console.error('[MEDICATION] Invalid time index:', index); + return; + } + } else { + setEditingTimeIndex(null); + setTimePickerDate(createDateFromTime(getDefaultTimeByIndex(medicationTimesPicker.length))); + } + setTimePickerVisible(true); + } catch (error) { + console.error('[MEDICATION] Error in openTimePicker:', error); + } + }, + [medicationTimesPicker, createDateFromTime, getDefaultTimeByIndex] + ); + + // 确认时间选择 + 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); + setMedicationTimesPicker((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, formatTime] + ); + + // 删除时间 + const removeTime = useCallback((index: number) => { + setMedicationTimesPicker((prev) => { + if (prev.length === 1) { + return prev; // 至少保留一个时间 + } + return prev.filter((_, idx) => idx !== index); + }); + // 同时更新 timesPerDayPicker + setTimesPerDayPicker((prev) => Math.max(1, prev - 1)); + }, []); + + // 添加时间 + const addTime = useCallback(() => { + openTimePicker(); + // 同时更新 timesPerDayPicker + setTimesPerDayPicker((prev) => prev + 1); + }, [openTimePicker]); if (!medicationId) { return ( @@ -505,7 +776,7 @@ export default function MedicationDetailScreen() { value={startDateLabel} icon="calendar-outline" colors={colors} - clickable={true} + clickable={false} onPress={handleStartDatePress} /> - + 频率 @@ -526,7 +801,7 @@ export default function MedicationDetailScreen() { {frequencyLabel} - +
@@ -601,12 +876,25 @@ export default function MedicationDetailScreen() { ]} > setDeleteSheetVisible(true)} > - - 删除该药品 + {isLiquidGlassAvailable() ? ( + + + 删除该药品 + + ) : ( + + + 删除该药品 + + )} ) : null} @@ -707,6 +995,303 @@ export default function MedicationDetailScreen() { + + setDosagePickerVisible(false)} + > + setDosagePickerVisible(false)} + /> + + + 选择剂量 + + + + + 剂量值 + + setDosageValuePicker(Number(value))} + itemStyle={styles.pickerItem} + style={styles.picker} + > + {DOSAGE_VALUES.map((value) => ( + + ))} + + + + + 单位 + + setDosageUnitPicker(String(value))} + itemStyle={styles.pickerItem} + style={styles.picker} + > + {DOSAGE_UNITS.map((unit) => ( + + ))} + + + + + setDosagePickerVisible(false)} + style={[styles.pickerBtn, { borderColor: colors.border }]} + > + + 取消 + + + + + 确定 + + + + + + + setFormPickerVisible(false)} + > + setFormPickerVisible(false)} + /> + + + 选择剂型 + + setFormPicker(value as MedicationForm)} + itemStyle={styles.pickerItem} + style={styles.picker} + > + {FORM_OPTIONS.map((option) => ( + + ))} + + + setFormPickerVisible(false)} + style={[styles.pickerBtn, { borderColor: colors.border }]} + > + + 取消 + + + + + 确定 + + + + + + + setFrequencyPickerVisible(false)} + > + setFrequencyPickerVisible(false)} + /> + + + 选择服药频率 + + + + + 重复模式 + + setRepeatPatternPicker(value as RepeatPattern)} + itemStyle={styles.pickerItem} + style={styles.picker} + > + + {/* + */} + + + + + 每日次数 + + setTimesPerDayPicker(Number(value))} + itemStyle={styles.pickerItem} + style={styles.picker} + > + {TIMES_PER_DAY_OPTIONS.map((times) => ( + + ))} + + + + + {/* 提醒时间列表 */} + + + 每日提醒时间 + + + {medicationTimesPicker.map((time, index) => ( + + openTimePicker(index)}> + + {time} + + removeTime(index)} disabled={medicationTimesPicker.length === 1} hitSlop={12}> + + + + ))} + + + 添加时间 + + + + + + setFrequencyPickerVisible(false)} + style={[styles.pickerBtn, { borderColor: colors.border }]} + > + + 取消 + + + + + 确定 + + + + + + + {/* 时间选择器 Modal */} + { + 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.pickerBtn, { borderColor: colors.border }]} + > + 取消 + + confirmTime(timePickerDate)} + style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]} + > + 确定 + + + )} + + + {medication ? ( void; - clickable?: boolean; -}) => { - const CardWrapper = clickable ? TouchableOpacity : View; - - return ( - - {clickable && ( - - - - )} - - - - {label} - {value} - - ); -}; const styles = StyleSheet.create({ container: { @@ -861,43 +1410,6 @@ const styles = StyleSheet.create({ flexDirection: 'row', gap: 12, }, - infoCard: { - flex: 1, - borderRadius: 20, - padding: 16, - backgroundColor: '#fff', - gap: 6, - shadowColor: '#000', - shadowOpacity: 0.04, - shadowRadius: 8, - shadowOffset: { width: 0, height: 4 }, - elevation: 2, - position: 'relative', - }, - infoCardArrow: { - position: 'absolute', - top: 12, - right: 12, - zIndex: 1, - }, - infoCardIcon: { - width: 28, - height: 28, - borderRadius: 14, - backgroundColor: '#EEF1FF', - alignItems: 'center', - justifyContent: 'center', - }, - infoCardLabel: { - fontSize: 13, - color: '#6B7280', - marginTop: 8, - }, - infoCardValue: { - fontSize: 16, - fontWeight: '600', - color: '#1F2933', - }, fullCard: { borderRadius: 22, padding: 18, @@ -1073,11 +1585,14 @@ const styles = StyleSheet.create({ deleteButton: { height: 56, borderRadius: 24, - backgroundColor: '#EF4444', 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, @@ -1089,4 +1604,109 @@ const styles = StyleSheet.create({ 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', + }, + // 时间列表相关样式 + timeListSection: { + gap: 12, + marginTop: 16, + }, + timeListScroll: { + maxHeight: 200, + }, + timeItemInPicker: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderWidth: 1, + borderRadius: 12, + paddingHorizontal: 14, + paddingVertical: 10, + marginBottom: 8, + }, + timeValue: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + timeTextSmall: { + fontSize: 16, + fontWeight: '600', + }, + addTimeButtonSmall: { + borderWidth: 1, + borderRadius: 12, + paddingVertical: 10, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 6, + }, + addTimeLabelSmall: { + fontSize: 13, + fontWeight: '600', + }, }); diff --git a/app/medications/add-medication.tsx b/app/medications/add-medication.tsx index 146212e..3f54de9 100644 --- a/app/medications/add-medication.tsx +++ b/app/medications/add-medication.tsx @@ -2,6 +2,7 @@ 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'; @@ -62,17 +63,6 @@ interface AddMedicationFormData { note: string; } -const FORM_OPTIONS: Array<{ id: MedicationForm; label: string; icon: keyof typeof MaterialCommunityIcons.glyphMap }> = [ - { id: 'capsule', label: '胶囊', icon: 'pill' }, - { id: 'pill', label: '药片', icon: 'tablet' }, - { id: 'injection', label: '注射', icon: 'needle' }, - { id: 'spray', label: '喷雾', icon: 'spray' }, - { id: 'drop', label: '滴剂', icon: 'eyedropper' }, - { id: 'syrup', label: '糖浆', icon: 'bottle-tonic' }, - { id: 'other', label: '其他', icon: 'dots-horizontal' }, -]; - -const DOSAGE_UNITS = ['片', '粒', '毫升', '滴', '喷', '勺']; const TIMES_PER_DAY_OPTIONS = Array.from({ length: 10 }, (_, index) => index + 1); const STEP_TITLES = ['药品名称', '剂型与剂量', '服药频率', '服药时间', '备注']; const STEP_DESCRIPTIONS = [ diff --git a/components/ui/InfoCard.tsx b/components/ui/InfoCard.tsx new file mode 100644 index 0000000..d3f4f1a --- /dev/null +++ b/components/ui/InfoCard.tsx @@ -0,0 +1,149 @@ +import type { Colors } from '@/constants/Colors'; +import { Ionicons } from '@expo/vector-icons'; +import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; +import React from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; + +export interface InfoCardProps { + label: string; + value: string; + icon: keyof typeof Ionicons.glyphMap; + colors: (typeof Colors)[keyof typeof Colors]; + onPress?: () => void; + clickable?: boolean; + glassEffectStyle?: 'clear' | 'regular'; + tintColor?: string; + className?: string; +} + +export const InfoCard: React.FC = ({ + label, + value, + icon, + colors, + onPress, + clickable = false, + glassEffectStyle = 'clear', + +}) => { + const isGlassAvailable = isLiquidGlassAvailable(); + + // 如果可点击且有onPress回调,使用TouchableOpacity包装 + if (clickable && onPress) { + return ( + + {isGlassAvailable ? ( + + + + + + + + {label} + {value} + + ) : ( + + + + + + + + {label} + {value} + + )} + + ); + } + + // 不可点击的版本 + return ( + + {isGlassAvailable ? ( + + + + + {label} + {value} + + ) : ( + + + + + {label} + {value} + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + infoCard: { + flex: 1, + borderRadius: 20, + padding: 16, + backgroundColor: '#fff', + gap: 6, + shadowColor: '#000', + shadowOpacity: 0.04, + shadowRadius: 8, + shadowOffset: { width: 0, height: 4 }, + elevation: 2, + position: 'relative', + overflow: 'hidden', // 保证玻璃边界圆角效果 + }, + infoCardArrow: { + position: 'absolute', + top: 12, + right: 12, + zIndex: 1, + }, + infoCardIcon: { + width: 28, + height: 28, + borderRadius: 14, + backgroundColor: '#EEF1FF', + alignItems: 'center', + justifyContent: 'center', + }, + infoCardLabel: { + fontSize: 13, + color: '#6B7280', + marginTop: 8, + }, + infoCardValue: { + fontSize: 16, + fontWeight: '600', + color: '#1F2933', + }, +}); + +export default InfoCard; \ No newline at end of file diff --git a/constants/Medication.ts b/constants/Medication.ts new file mode 100644 index 0000000..6e9a2f2 --- /dev/null +++ b/constants/Medication.ts @@ -0,0 +1,56 @@ +/** + * 药物管理相关常量 + */ + +import type { MedicationForm } from '@/types/medication'; +import type { MaterialCommunityIcons } from '@expo/vector-icons'; + +/** + * 剂型选项配置 + */ +export const FORM_OPTIONS: Array<{ + id: MedicationForm; + label: string; + icon: keyof typeof MaterialCommunityIcons.glyphMap; +}> = [ + { id: 'capsule', label: '胶囊', icon: 'pill' }, + { id: 'pill', label: '药片', icon: 'tablet' }, + { id: 'injection', label: '注射', icon: 'needle' }, + { id: 'spray', label: '喷雾', icon: 'spray' }, + { id: 'drop', label: '滴剂', icon: 'eyedropper' }, + { id: 'syrup', label: '糖浆', icon: 'bottle-tonic' }, + { id: 'other', label: '其他', icon: 'dots-horizontal' }, +]; + +/** + * 剂型标签映射 + */ +export const FORM_LABELS: Record = { + capsule: '胶囊', + pill: '药片', + injection: '注射', + spray: '喷雾', + drop: '滴剂', + syrup: '糖浆', + other: '其他', +}; + +/** + * 剂量单位选项 + */ +export const DOSAGE_UNITS = ['片', '粒', '毫升', '滴', '喷', '勺']; + +/** + * 剂量值选项 (0.5 - 10,步长0.5) + */ +export const DOSAGE_VALUES = Array.from({ length: 20 }, (_, i) => (i + 1) * 0.5); + +/** + * 每日次数选项 + */ +export const TIMES_PER_DAY_OPTIONS = Array.from({ length: 10 }, (_, index) => index + 1); + +/** + * 默认服药时间预设 + */ +export const DEFAULT_TIME_PRESETS = ['08:00', '12:00', '18:00', '22:00']; \ No newline at end of file