feat(medications): 增强药品详情页面的编辑功能
- 添加剂量、剂型和服药频率的交互式选择器 - 实现提醒时间的动态编辑和添加功能 - 引入玻璃效果优化删除按钮的视觉体验 - 重构常量配置,提取药物相关常量到独立文件 - 创建可复用的InfoCard组件支持玻璃效果
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import { ThemedText } from '@/components/ThemedText';
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
import { ConfirmationSheet } from '@/components/ui/ConfirmationSheet';
|
import { ConfirmationSheet } from '@/components/ui/ConfirmationSheet';
|
||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
|
import InfoCard from '@/components/ui/InfoCard';
|
||||||
import { Colors } from '@/constants/Colors';
|
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 { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { getMedicationById, getMedicationRecords } from '@/services/medications';
|
import { getMedicationById, getMedicationRecords } from '@/services/medications';
|
||||||
@@ -11,10 +13,13 @@ import {
|
|||||||
selectMedications,
|
selectMedications,
|
||||||
updateMedicationAction,
|
updateMedicationAction,
|
||||||
} from '@/store/medicationsSlice';
|
} from '@/store/medicationsSlice';
|
||||||
import type { Medication } from '@/types/medication';
|
import type { Medication, MedicationForm, RepeatPattern } from '@/types/medication';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
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 Voice from '@react-native-voice/voice';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
@@ -24,6 +29,7 @@ import {
|
|||||||
Keyboard,
|
Keyboard,
|
||||||
Modal,
|
Modal,
|
||||||
Platform,
|
Platform,
|
||||||
|
Pressable,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -34,16 +40,6 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
const FORM_LABELS: Record<Medication['form'], string> = {
|
|
||||||
capsule: '胶囊',
|
|
||||||
pill: '药片',
|
|
||||||
injection: '注射',
|
|
||||||
spray: '喷雾',
|
|
||||||
drop: '滴剂',
|
|
||||||
syrup: '糖浆',
|
|
||||||
other: '其他',
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_IMAGE = require('@/assets/images/medicine/image-medicine.png');
|
const DEFAULT_IMAGE = require('@/assets/images/medicine/image-medicine.png');
|
||||||
|
|
||||||
type RecordsSummary = {
|
type RecordsSummary = {
|
||||||
@@ -84,6 +80,84 @@ export default function MedicationDetailScreen() {
|
|||||||
const [deleteSheetVisible, setDeleteSheetVisible] = useState(false);
|
const [deleteSheetVisible, setDeleteSheetVisible] = useState(false);
|
||||||
const [deleteLoading, setDeleteLoading] = 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<MedicationForm>(
|
||||||
|
medicationFromStore?.form ?? 'capsule'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 频率选择相关状态
|
||||||
|
const [frequencyPickerVisible, setFrequencyPickerVisible] = useState(false);
|
||||||
|
const [repeatPatternPicker, setRepeatPatternPicker] = useState<RepeatPattern>(
|
||||||
|
medicationFromStore?.repeatPattern ?? 'daily'
|
||||||
|
);
|
||||||
|
const [timesPerDayPicker, setTimesPerDayPicker] = useState(
|
||||||
|
medicationFromStore?.timesPerDay ?? 1
|
||||||
|
);
|
||||||
|
|
||||||
|
// 提醒时间相关状态
|
||||||
|
const [timePickerVisible, setTimePickerVisible] = useState(false);
|
||||||
|
const [timePickerDate, setTimePickerDate] = useState<Date>(new Date());
|
||||||
|
const [editingTimeIndex, setEditingTimeIndex] = useState<number | null>(null);
|
||||||
|
const [medicationTimesPicker, setMedicationTimesPicker] = useState<string[]>(
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (!medicationFromStore) {
|
if (!medicationFromStore) {
|
||||||
dispatch(fetchMedications());
|
dispatch(fetchMedications());
|
||||||
@@ -97,6 +171,37 @@ export default function MedicationDetailScreen() {
|
|||||||
}
|
}
|
||||||
}, [medicationFromStore]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
setNoteDraft(medication?.note ?? '');
|
setNoteDraft(medication?.note ?? '');
|
||||||
}, [medication?.note]);
|
}, [medication?.note]);
|
||||||
@@ -422,12 +527,178 @@ export default function MedicationDetailScreen() {
|
|||||||
}, [reminderTimes]);
|
}, [reminderTimes]);
|
||||||
|
|
||||||
const handleDosagePress = useCallback(() => {
|
const handleDosagePress = useCallback(() => {
|
||||||
Alert.alert('每次剂量', `单次服用剂量:${dosageLabel}`);
|
if (!medication) return;
|
||||||
}, [dosageLabel]);
|
setDosagePickerVisible(true);
|
||||||
|
}, [medication]);
|
||||||
|
|
||||||
const handleFormPress = useCallback(() => {
|
const handleFormPress = useCallback(() => {
|
||||||
Alert.alert('剂型', `药品剂型:${formLabel}`);
|
if (!medication) return;
|
||||||
}, [formLabel]);
|
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) {
|
if (!medicationId) {
|
||||||
return (
|
return (
|
||||||
@@ -505,7 +776,7 @@ export default function MedicationDetailScreen() {
|
|||||||
value={startDateLabel}
|
value={startDateLabel}
|
||||||
icon="calendar-outline"
|
icon="calendar-outline"
|
||||||
colors={colors}
|
colors={colors}
|
||||||
clickable={true}
|
clickable={false}
|
||||||
onPress={handleStartDatePress}
|
onPress={handleStartDatePress}
|
||||||
/>
|
/>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
@@ -513,11 +784,15 @@ export default function MedicationDetailScreen() {
|
|||||||
value={reminderTimes}
|
value={reminderTimes}
|
||||||
icon="time-outline"
|
icon="time-outline"
|
||||||
colors={colors}
|
colors={colors}
|
||||||
clickable={true}
|
clickable={false}
|
||||||
onPress={handleTimePress}
|
onPress={handleTimePress}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.fullCard, { backgroundColor: colors.surface }]}>
|
<TouchableOpacity
|
||||||
|
style={[styles.fullCard, { backgroundColor: colors.surface }]}
|
||||||
|
onPress={handleFrequencyPress}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
<View style={styles.fullCardLeading}>
|
<View style={styles.fullCardLeading}>
|
||||||
<Ionicons name="repeat-outline" size={18} color={colors.primary} />
|
<Ionicons name="repeat-outline" size={18} color={colors.primary} />
|
||||||
<Text style={[styles.fullCardLabel, { color: colors.text }]}>频率</Text>
|
<Text style={[styles.fullCardLabel, { color: colors.text }]}>频率</Text>
|
||||||
@@ -526,7 +801,7 @@ export default function MedicationDetailScreen() {
|
|||||||
<Text style={[styles.fullCardValue, { color: colors.text }]}>{frequencyLabel}</Text>
|
<Text style={[styles.fullCardValue, { color: colors.text }]}>{frequencyLabel}</Text>
|
||||||
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</TouchableOpacity>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="剂量与形式" color={colors.text}>
|
<Section title="剂量与形式" color={colors.text}>
|
||||||
@@ -601,12 +876,25 @@ export default function MedicationDetailScreen() {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.deleteButton}
|
|
||||||
activeOpacity={0.9}
|
activeOpacity={0.9}
|
||||||
onPress={() => setDeleteSheetVisible(true)}
|
onPress={() => setDeleteSheetVisible(true)}
|
||||||
|
>
|
||||||
|
{isLiquidGlassAvailable() ? (
|
||||||
|
<GlassView
|
||||||
|
style={styles.deleteButton}
|
||||||
|
glassEffectStyle="regular"
|
||||||
|
tintColor="rgba(239, 68, 68, 0.8)"
|
||||||
|
isInteractive={true}
|
||||||
>
|
>
|
||||||
<Ionicons name='trash-outline' size={18} color='#fff' />
|
<Ionicons name='trash-outline' size={18} color='#fff' />
|
||||||
<Text style={styles.deleteButtonText}>删除该药品</Text>
|
<Text style={styles.deleteButtonText}>删除该药品</Text>
|
||||||
|
</GlassView>
|
||||||
|
) : (
|
||||||
|
<View style={[styles.deleteButton, styles.fallbackDeleteButton]}>
|
||||||
|
<Ionicons name='trash-outline' size={18} color='#fff' />
|
||||||
|
<Text style={styles.deleteButtonText}>删除该药品</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -707,6 +995,303 @@ export default function MedicationDetailScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
visible={dosagePickerVisible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setDosagePickerVisible(false)}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
style={styles.pickerBackdrop}
|
||||||
|
onPress={() => setDosagePickerVisible(false)}
|
||||||
|
/>
|
||||||
|
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}>
|
||||||
|
<ThemedText style={[styles.pickerTitle, { color: colors.text }]}>
|
||||||
|
选择剂量
|
||||||
|
</ThemedText>
|
||||||
|
<View style={styles.pickerRow}>
|
||||||
|
<View style={styles.pickerColumn}>
|
||||||
|
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary }]}>
|
||||||
|
剂量值
|
||||||
|
</ThemedText>
|
||||||
|
<Picker
|
||||||
|
selectedValue={dosageValuePicker}
|
||||||
|
onValueChange={(value) => setDosageValuePicker(Number(value))}
|
||||||
|
itemStyle={styles.pickerItem}
|
||||||
|
style={styles.picker}
|
||||||
|
>
|
||||||
|
{DOSAGE_VALUES.map((value) => (
|
||||||
|
<Picker.Item
|
||||||
|
key={value}
|
||||||
|
label={String(value)}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
<View style={styles.pickerColumn}>
|
||||||
|
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary }]}>
|
||||||
|
单位
|
||||||
|
</ThemedText>
|
||||||
|
<Picker
|
||||||
|
selectedValue={dosageUnitPicker}
|
||||||
|
onValueChange={(value) => setDosageUnitPicker(String(value))}
|
||||||
|
itemStyle={styles.pickerItem}
|
||||||
|
style={styles.picker}
|
||||||
|
>
|
||||||
|
{DOSAGE_UNITS.map((unit) => (
|
||||||
|
<Picker.Item
|
||||||
|
key={unit}
|
||||||
|
label={unit}
|
||||||
|
value={unit}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.pickerActions}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setDosagePickerVisible(false)}
|
||||||
|
style={[styles.pickerBtn, { borderColor: colors.border }]}
|
||||||
|
>
|
||||||
|
<ThemedText style={[styles.pickerBtnText, { color: colors.textSecondary }]}>
|
||||||
|
取消
|
||||||
|
</ThemedText>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={confirmDosagePicker}
|
||||||
|
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
|
||||||
|
>
|
||||||
|
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}>
|
||||||
|
确定
|
||||||
|
</ThemedText>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
visible={formPickerVisible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setFormPickerVisible(false)}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
style={styles.pickerBackdrop}
|
||||||
|
onPress={() => setFormPickerVisible(false)}
|
||||||
|
/>
|
||||||
|
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}>
|
||||||
|
<ThemedText style={[styles.pickerTitle, { color: colors.text }]}>
|
||||||
|
选择剂型
|
||||||
|
</ThemedText>
|
||||||
|
<Picker
|
||||||
|
selectedValue={formPicker}
|
||||||
|
onValueChange={(value) => setFormPicker(value as MedicationForm)}
|
||||||
|
itemStyle={styles.pickerItem}
|
||||||
|
style={styles.picker}
|
||||||
|
>
|
||||||
|
{FORM_OPTIONS.map((option) => (
|
||||||
|
<Picker.Item
|
||||||
|
key={option.id}
|
||||||
|
label={option.label}
|
||||||
|
value={option.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Picker>
|
||||||
|
<View style={styles.pickerActions}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setFormPickerVisible(false)}
|
||||||
|
style={[styles.pickerBtn, { borderColor: colors.border }]}
|
||||||
|
>
|
||||||
|
<ThemedText style={[styles.pickerBtnText, { color: colors.textSecondary }]}>
|
||||||
|
取消
|
||||||
|
</ThemedText>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={confirmFormPicker}
|
||||||
|
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
|
||||||
|
>
|
||||||
|
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}>
|
||||||
|
确定
|
||||||
|
</ThemedText>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
visible={frequencyPickerVisible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setFrequencyPickerVisible(false)}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
style={styles.pickerBackdrop}
|
||||||
|
onPress={() => setFrequencyPickerVisible(false)}
|
||||||
|
/>
|
||||||
|
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}>
|
||||||
|
<ThemedText style={[styles.pickerTitle, { color: colors.text }]}>
|
||||||
|
选择服药频率
|
||||||
|
</ThemedText>
|
||||||
|
<View style={styles.pickerRow}>
|
||||||
|
<View style={styles.pickerColumn}>
|
||||||
|
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary }]}>
|
||||||
|
重复模式
|
||||||
|
</ThemedText>
|
||||||
|
<Picker
|
||||||
|
selectedValue={repeatPatternPicker}
|
||||||
|
onValueChange={(value) => setRepeatPatternPicker(value as RepeatPattern)}
|
||||||
|
itemStyle={styles.pickerItem}
|
||||||
|
style={styles.picker}
|
||||||
|
>
|
||||||
|
<Picker.Item label="每日" value="daily" />
|
||||||
|
{/* <Picker.Item label="每周" value="weekly" />
|
||||||
|
<Picker.Item label="自定义" value="custom" /> */}
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
<View style={styles.pickerColumn}>
|
||||||
|
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary }]}>
|
||||||
|
每日次数
|
||||||
|
</ThemedText>
|
||||||
|
<Picker
|
||||||
|
selectedValue={timesPerDayPicker}
|
||||||
|
onValueChange={(value) => setTimesPerDayPicker(Number(value))}
|
||||||
|
itemStyle={styles.pickerItem}
|
||||||
|
style={styles.picker}
|
||||||
|
>
|
||||||
|
{TIMES_PER_DAY_OPTIONS.map((times) => (
|
||||||
|
<Picker.Item
|
||||||
|
key={times}
|
||||||
|
label={`${times} 次`}
|
||||||
|
value={times}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 提醒时间列表 */}
|
||||||
|
<View style={styles.timeListSection}>
|
||||||
|
<ThemedText style={[styles.pickerLabel, { color: colors.textSecondary, marginBottom: 8 }]}>
|
||||||
|
每日提醒时间
|
||||||
|
</ThemedText>
|
||||||
|
<ScrollView style={styles.timeListScroll} showsVerticalScrollIndicator={false}>
|
||||||
|
{medicationTimesPicker.map((time, index) => (
|
||||||
|
<View
|
||||||
|
key={`${time}-${index}`}
|
||||||
|
style={[
|
||||||
|
styles.timeItemInPicker,
|
||||||
|
{
|
||||||
|
borderColor: `${colors.border}80`,
|
||||||
|
backgroundColor: colors.pageBackgroundEmphasis,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TouchableOpacity style={styles.timeValue} onPress={() => openTimePicker(index)}>
|
||||||
|
<Ionicons name="time" size={16} color={colors.primary} />
|
||||||
|
<ThemedText style={[styles.timeTextSmall, { color: colors.text }]}>{time}</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Pressable onPress={() => removeTime(index)} disabled={medicationTimesPicker.length === 1} hitSlop={12}>
|
||||||
|
<Ionicons
|
||||||
|
name="close-circle"
|
||||||
|
size={16}
|
||||||
|
color={medicationTimesPicker.length === 1 ? `${colors.border}80` : colors.textSecondary}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.addTimeButtonSmall, { borderColor: colors.primary }]}
|
||||||
|
onPress={addTime}
|
||||||
|
>
|
||||||
|
<Ionicons name="add" size={14} color={colors.primary} />
|
||||||
|
<ThemedText style={[styles.addTimeLabelSmall, { color: colors.primary }]}>添加时间</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.pickerActions}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setFrequencyPickerVisible(false)}
|
||||||
|
style={[styles.pickerBtn, { borderColor: colors.border }]}
|
||||||
|
>
|
||||||
|
<ThemedText style={[styles.pickerBtnText, { color: colors.textSecondary }]}>
|
||||||
|
取消
|
||||||
|
</ThemedText>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={confirmFrequencyPicker}
|
||||||
|
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
|
||||||
|
>
|
||||||
|
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}>
|
||||||
|
确定
|
||||||
|
</ThemedText>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 时间选择器 Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={timePickerVisible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => {
|
||||||
|
setTimePickerVisible(false);
|
||||||
|
setEditingTimeIndex(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
style={styles.pickerBackdrop}
|
||||||
|
onPress={() => {
|
||||||
|
setTimePickerVisible(false);
|
||||||
|
setEditingTimeIndex(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}>
|
||||||
|
<ThemedText style={[styles.pickerTitle, { color: colors.text }]}>
|
||||||
|
{editingTimeIndex !== null ? '修改提醒时间' : '添加提醒时间'}
|
||||||
|
</ThemedText>
|
||||||
|
<DateTimePicker
|
||||||
|
value={timePickerDate}
|
||||||
|
mode="time"
|
||||||
|
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
||||||
|
onChange={(event, date) => {
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
if (date) setTimePickerDate(date);
|
||||||
|
} else {
|
||||||
|
if (event.type === 'set' && date) {
|
||||||
|
confirmTime(date);
|
||||||
|
} else {
|
||||||
|
setTimePickerVisible(false);
|
||||||
|
setEditingTimeIndex(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{Platform.OS === 'ios' && (
|
||||||
|
<View style={styles.pickerActions}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
setTimePickerVisible(false);
|
||||||
|
setEditingTimeIndex(null);
|
||||||
|
}}
|
||||||
|
style={[styles.pickerBtn, { borderColor: colors.border }]}
|
||||||
|
>
|
||||||
|
<ThemedText style={[styles.pickerBtnText, { color: colors.textSecondary }]}>取消</ThemedText>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => confirmTime(timePickerDate)}
|
||||||
|
style={[styles.pickerBtn, styles.pickerBtnPrimary, { backgroundColor: colors.primary }]}
|
||||||
|
>
|
||||||
|
<ThemedText style={[styles.pickerBtnText, { color: colors.onPrimary }]}>确定</ThemedText>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{medication ? (
|
{medication ? (
|
||||||
<ConfirmationSheet
|
<ConfirmationSheet
|
||||||
visible={deleteSheetVisible}
|
visible={deleteSheetVisible}
|
||||||
@@ -741,42 +1326,6 @@ const Section = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const InfoCard = ({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
icon,
|
|
||||||
colors,
|
|
||||||
onPress,
|
|
||||||
clickable = false,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
icon: keyof typeof Ionicons.glyphMap;
|
|
||||||
colors: (typeof Colors)[keyof typeof Colors];
|
|
||||||
onPress?: () => void;
|
|
||||||
clickable?: boolean;
|
|
||||||
}) => {
|
|
||||||
const CardWrapper = clickable ? TouchableOpacity : View;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardWrapper
|
|
||||||
style={[styles.infoCard, { backgroundColor: colors.surface }]}
|
|
||||||
onPress={onPress}
|
|
||||||
activeOpacity={clickable ? 0.7 : 1}
|
|
||||||
>
|
|
||||||
{clickable && (
|
|
||||||
<View style={styles.infoCardArrow}>
|
|
||||||
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View style={styles.infoCardIcon}>
|
|
||||||
<Ionicons name={icon} size={16} color="#4C6EF5" />
|
|
||||||
</View>
|
|
||||||
<Text style={[styles.infoCardLabel, { color: colors.textSecondary }]}>{label}</Text>
|
|
||||||
<Text style={[styles.infoCardValue, { color: colors.text }]}>{value}</Text>
|
|
||||||
</CardWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
@@ -861,43 +1410,6 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: 12,
|
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: {
|
fullCard: {
|
||||||
borderRadius: 22,
|
borderRadius: 22,
|
||||||
padding: 18,
|
padding: 18,
|
||||||
@@ -1073,11 +1585,14 @@ const styles = StyleSheet.create({
|
|||||||
deleteButton: {
|
deleteButton: {
|
||||||
height: 56,
|
height: 56,
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
backgroundColor: '#EF4444',
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
|
overflow: 'hidden', // 保证玻璃边界圆角效果
|
||||||
|
},
|
||||||
|
fallbackDeleteButton: {
|
||||||
|
backgroundColor: '#EF4444',
|
||||||
shadowColor: 'rgba(239,68,68,0.4)',
|
shadowColor: 'rgba(239,68,68,0.4)',
|
||||||
shadowOffset: { width: 0, height: 10 },
|
shadowOffset: { width: 0, height: 10 },
|
||||||
shadowOpacity: 1,
|
shadowOpacity: 1,
|
||||||
@@ -1089,4 +1604,109 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: '#fff',
|
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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ThemedText } from '@/components/ThemedText';
|
|||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { DOSAGE_UNITS, FORM_OPTIONS } from '@/constants/Medication';
|
||||||
import { useAppDispatch } from '@/hooks/redux';
|
import { useAppDispatch } from '@/hooks/redux';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
@@ -62,17 +63,6 @@ interface AddMedicationFormData {
|
|||||||
note: string;
|
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 TIMES_PER_DAY_OPTIONS = Array.from({ length: 10 }, (_, index) => index + 1);
|
||||||
const STEP_TITLES = ['药品名称', '剂型与剂量', '服药频率', '服药时间', '备注'];
|
const STEP_TITLES = ['药品名称', '剂型与剂量', '服药频率', '服药时间', '备注'];
|
||||||
const STEP_DESCRIPTIONS = [
|
const STEP_DESCRIPTIONS = [
|
||||||
|
|||||||
149
components/ui/InfoCard.tsx
Normal file
149
components/ui/InfoCard.tsx
Normal file
@@ -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<InfoCardProps> = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
icon,
|
||||||
|
colors,
|
||||||
|
onPress,
|
||||||
|
clickable = false,
|
||||||
|
glassEffectStyle = 'clear',
|
||||||
|
|
||||||
|
}) => {
|
||||||
|
const isGlassAvailable = isLiquidGlassAvailable();
|
||||||
|
|
||||||
|
// 如果可点击且有onPress回调,使用TouchableOpacity包装
|
||||||
|
if (clickable && onPress) {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.container}
|
||||||
|
onPress={onPress}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
{isGlassAvailable ? (
|
||||||
|
<GlassView
|
||||||
|
style={[
|
||||||
|
styles.infoCard,
|
||||||
|
]}
|
||||||
|
glassEffectStyle={glassEffectStyle}
|
||||||
|
isInteractive={true}
|
||||||
|
>
|
||||||
|
<View style={styles.infoCardArrow}>
|
||||||
|
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.infoCardIcon}>
|
||||||
|
<Ionicons name={icon} size={16} color="#4C6EF5" />
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.infoCardLabel, { color: colors.textSecondary }]}>{label}</Text>
|
||||||
|
<Text style={[styles.infoCardValue, { color: colors.text }]}>{value}</Text>
|
||||||
|
</GlassView>
|
||||||
|
) : (
|
||||||
|
<View style={[styles.infoCard]}>
|
||||||
|
<View style={styles.infoCardArrow}>
|
||||||
|
<Ionicons name="chevron-forward" size={16} color={colors.textMuted} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.infoCardIcon}>
|
||||||
|
<Ionicons name={icon} size={16} color="#4C6EF5" />
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.infoCardLabel, { color: colors.textSecondary }]}>{label}</Text>
|
||||||
|
<Text style={[styles.infoCardValue, { color: colors.text }]}>{value}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不可点击的版本
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{isGlassAvailable ? (
|
||||||
|
<GlassView
|
||||||
|
style={[
|
||||||
|
styles.infoCard,
|
||||||
|
{
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
borderColor: `${colors.border}80`,
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
glassEffectStyle={glassEffectStyle}
|
||||||
|
>
|
||||||
|
<View style={styles.infoCardIcon}>
|
||||||
|
<Ionicons name={icon} size={16} color="#4C6EF5" />
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.infoCardLabel, { color: colors.textSecondary }]}>{label}</Text>
|
||||||
|
<Text style={[styles.infoCardValue, { color: colors.text }]}>{value}</Text>
|
||||||
|
</GlassView>
|
||||||
|
) : (
|
||||||
|
<View style={[styles.infoCard, { backgroundColor: colors.surface }]}>
|
||||||
|
<View style={styles.infoCardIcon}>
|
||||||
|
<Ionicons name={icon} size={16} color="#4C6EF5" />
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.infoCardLabel, { color: colors.textSecondary }]}>{label}</Text>
|
||||||
|
<Text style={[styles.infoCardValue, { color: colors.text }]}>{value}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
56
constants/Medication.ts
Normal file
56
constants/Medication.ts
Normal file
@@ -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<MedicationForm, string> = {
|
||||||
|
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'];
|
||||||
Reference in New Issue
Block a user