feat(medications): 添加药品结束日期选择功能

- 新增药品结束日期选择器,支持设置服药周期
- 优化日期显示格式,从"开始日期"改为"服药周期"
- 添加日期验证逻辑,确保开始日期不早于今天且结束日期不早于开始日期
- 改进添加药品页面的日期选择UI,采用并排布局
- 调整InfoCard组件样式,移除图标背景色并减小字体大小
This commit is contained in:
richarjiang
2025-11-12 10:27:20 +08:00
parent e412f80295
commit 35f06951a0
3 changed files with 206 additions and 40 deletions

View File

@@ -378,6 +378,23 @@ export default function MedicationDetailScreen() {
const startDateLabel = medication const startDateLabel = medication
? dayjs(medication.startDate).format('YYYY年M月D日') ? dayjs(medication.startDate).format('YYYY年M月D日')
: '--'; : '--';
// 计算服药周期显示
const medicationPeriodLabel = useMemo(() => {
if (!medication) return '--';
const startDate = dayjs(medication.startDate).format('YYYY年M月D日');
if (medication.endDate) {
// 有结束日期,显示开始日期到结束日期
const endDate = dayjs(medication.endDate).format('YYYY年M月D日');
return `${startDate} - ${endDate}`;
} else {
// 没有结束日期,显示长期
return `${startDate} - 长期`;
}
}, [medication]);
const reminderTimes = medication?.medicationTimes?.length const reminderTimes = medication?.medicationTimes?.length
? medication.medicationTimes.join('、') ? medication.medicationTimes.join('、')
: '尚未设置'; : '尚未设置';
@@ -467,8 +484,20 @@ export default function MedicationDetailScreen() {
}, [medication?.photoUrl]); }, [medication?.photoUrl]);
const handleStartDatePress = useCallback(() => { const handleStartDatePress = useCallback(() => {
Alert.alert('开始日期', `开始服药日期:${startDateLabel}`); if (!medication) return;
}, [startDateLabel]);
const startDate = dayjs(medication.startDate).format('YYYY年M月D日');
let message = `开始服药日期:${startDate}`;
if (medication.endDate) {
const endDate = dayjs(medication.endDate).format('YYYY年M月D日');
message += `\n结束服药日期${endDate}`;
} else {
message += `\n服药计划长期服药`;
}
Alert.alert('服药周期', message);
}, [medication]);
const handleTimePress = useCallback(() => { const handleTimePress = useCallback(() => {
Alert.alert('服药时间', `设置的时间:${reminderTimes}`); Alert.alert('服药时间', `设置的时间:${reminderTimes}`);
@@ -676,15 +705,15 @@ export default function MedicationDetailScreen() {
<Section title="服药计划" color={colors.text}> <Section title="服药计划" color={colors.text}>
<View style={styles.row}> <View style={styles.row}>
<InfoCard <InfoCard
label="开始日期" label="服药周期"
value={startDateLabel} value={medicationPeriodLabel}
icon="calendar-outline" icon="calendar-outline"
colors={colors} colors={colors}
clickable={false} clickable={false}
onPress={handleStartDatePress} onPress={handleStartDatePress}
/> />
<InfoCard <InfoCard
label="时间" label="用药时间"
value={reminderTimes} value={reminderTimes}
icon="time-outline" icon="time-outline"
colors={colors} colors={colors}

View File

@@ -128,7 +128,7 @@ export default function AddMedicationScreen() {
const { upload, uploading } = useCosUpload({ prefix: 'images/medications' }); const { upload, uploading } = useCosUpload({ prefix: 'images/medications' });
// 获取登录验证相关的功能 // 获取登录验证相关的功能
const { ensureLoggedIn } = useAuthGuard(); const { ensureLoggedIn } = useAuthGuard();
const softBorderColor = useMemo(() => withAlpha(colors.border, 0.45), [colors.border]); const softBorderColor = useMemo(() => withAlpha(colors.border, 0.25), [colors.border]);
const fadedBorderFill = useMemo(() => withAlpha('#ffffff', 1), [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 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 glassDisabledTint = useMemo(() => withAlpha(colors.border, theme === 'dark' ? 0.45 : 0.6), [colors.border, theme]);
@@ -146,8 +146,12 @@ export default function AddMedicationScreen() {
const [timesPickerVisible, setTimesPickerVisible] = useState(false); const [timesPickerVisible, setTimesPickerVisible] = useState(false);
const [timesPickerValue, setTimesPickerValue] = useState(1); const [timesPickerValue, setTimesPickerValue] = useState(1);
const [startDate, setStartDate] = useState<Date>(new Date()); const [startDate, setStartDate] = useState<Date>(new Date());
const [endDate, setEndDate] = useState<Date | null>(null);
const [datePickerVisible, setDatePickerVisible] = useState(false); const [datePickerVisible, setDatePickerVisible] = useState(false);
const [datePickerValue, setDatePickerValue] = useState<Date>(new Date()); const [datePickerValue, setDatePickerValue] = useState<Date>(new Date());
const [endDatePickerVisible, setEndDatePickerVisible] = useState(false);
const [endDatePickerValue, setEndDatePickerValue] = useState<Date>(new Date());
const [datePickerMode, setDatePickerMode] = useState<'start' | 'end'>('start');
const [medicationTimes, setMedicationTimes] = useState<string[]>([DEFAULT_TIME_PRESETS[0]]); const [medicationTimes, setMedicationTimes] = useState<string[]>([DEFAULT_TIME_PRESETS[0]]);
const [timePickerVisible, setTimePickerVisible] = useState(false); const [timePickerVisible, setTimePickerVisible] = useState(false);
const [timePickerDate, setTimePickerDate] = useState<Date>(createDateFromTime(DEFAULT_TIME_PRESETS[0])); const [timePickerDate, setTimePickerDate] = useState<Date>(createDateFromTime(DEFAULT_TIME_PRESETS[0]));
@@ -276,6 +280,7 @@ export default function AddMedicationScreen() {
timesPerDay: timesPerDay, timesPerDay: timesPerDay,
medicationTimes: medicationTimes, medicationTimes: medicationTimes,
startDate: dayjs(startDate).startOf('day').toISOString(), // ISO 8601 格式 startDate: dayjs(startDate).startOf('day').toISOString(), // ISO 8601 格式
endDate: endDate ? dayjs(endDate).endOf('day').toISOString() : undefined, // 如果有结束日期,设置为当天结束时间
repeatPattern: 'daily' as RepeatPattern, repeatPattern: 'daily' as RepeatPattern,
note: note.trim() || undefined, note: note.trim() || undefined,
}; };
@@ -332,6 +337,7 @@ export default function AddMedicationScreen() {
dosageUnit, dosageUnit,
medicationTimes, medicationTimes,
startDate, startDate,
endDate,
note, note,
dispatch, dispatch,
ensureLoggedIn, ensureLoggedIn,
@@ -469,15 +475,47 @@ export default function AddMedicationScreen() {
setPhotoUrl(null); setPhotoUrl(null);
}, []); }, []);
const openDatePicker = useCallback(() => { const openStartDatePicker = useCallback(() => {
setDatePickerValue(startDate); setDatePickerValue(startDate);
setDatePickerVisible(true); setDatePickerVisible(true);
}, [startDate]); }, [startDate]);
const openEndDatePicker = useCallback(() => {
setEndDatePickerValue(endDate || new Date());
setEndDatePickerVisible(true);
}, [endDate]);
const confirmStartDate = useCallback((date: Date) => { 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); setStartDate(date);
setDatePickerVisible(false); 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 openTimePicker = useCallback( const openTimePicker = useCallback(
(index?: number) => { (index?: number) => {
@@ -718,7 +756,6 @@ export default function AddMedicationScreen() {
case 2: case 2:
return ( return (
<View style={styles.stepSection}> <View style={styles.stepSection}>
<View style={styles.inputGroup}> <View style={styles.inputGroup}>
<ThemedText style={[styles.groupLabel, { color: colors.textSecondary }]}></ThemedText> <ThemedText style={[styles.groupLabel, { color: colors.textSecondary }]}></ThemedText>
<TouchableOpacity <TouchableOpacity
@@ -736,6 +773,57 @@ export default function AddMedicationScreen() {
<Ionicons name="chevron-down" size={18} color={colors.textSecondary} /> <Ionicons name="chevron-down" size={18} color={colors.textSecondary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={styles.inputGroup}>
<View style={styles.periodHeader}>
<ThemedText style={[styles.groupLabel, { color: colors.textSecondary }]}></ThemedText>
</View>
<View style={styles.dateRowContainer}>
<TouchableOpacity
activeOpacity={0.85}
style={[
styles.dateRow,
styles.dateRowHalf,
{
borderColor: softBorderColor,
backgroundColor: colors.surface,
},
]}
onPress={openStartDatePicker}
>
<View style={styles.dateLeft}>
<Ionicons name="calendar" size={16} color={colors.textSecondary} />
<ThemedText style={[styles.dateLabel, { color: colors.textMuted }]}></ThemedText>
<ThemedText style={[styles.dateValue, { color: colors.text }]}>
{dayjs(startDate).format('MM/DD')}
</ThemedText>
</View>
<Ionicons name="chevron-forward" size={16} color={colors.textSecondary} />
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.85}
style={[
styles.dateRow,
styles.dateRowHalf,
{
borderColor: softBorderColor,
backgroundColor: colors.surface,
},
]}
onPress={openEndDatePicker}
>
<View style={styles.dateLeft}>
<Ionicons name="calendar-outline" size={16} color={colors.textSecondary} />
<ThemedText style={[styles.dateLabel, { color: colors.textMuted }]}></ThemedText>
<ThemedText style={[styles.dateValue, { color: colors.text }]}>
{endDate ? dayjs(endDate).format('MM/DD') : '长期'}
</ThemedText>
</View>
<Ionicons name="chevron-forward" size={16} color={colors.textSecondary} />
</TouchableOpacity>
</View>
</View>
</View> </View>
); );
case 3: case 3:
@@ -895,30 +983,6 @@ export default function AddMedicationScreen() {
<View style={styles.contentContainer}>{renderStepContent()}</View> <View style={styles.contentContainer}>{renderStepContent()}</View>
<View style={styles.footer}> <View style={styles.footer}>
{showDateField && (
<TouchableOpacity
activeOpacity={0.85}
style={[
styles.startDateRow,
{
borderColor: softBorderColor,
backgroundColor: colors.surface,
},
]}
onPress={openDatePicker}
>
<View style={styles.startDateLeft}>
<Ionicons name="calendar" size={18} color={colors.textSecondary} />
<View>
<ThemedText style={[styles.startDateLabel, { color: colors.textMuted }]}></ThemedText>
<ThemedText style={[styles.startDateValue, { color: colors.text }]}>
{dayjs(startDate).format('YYYY 年 MM 月 DD 日')}
</ThemedText>
</View>
</View>
<Ionicons name="chevron-forward" size={18} color={colors.textSecondary} />
</TouchableOpacity>
)}
<View style={styles.footerButtons}> <View style={styles.footerButtons}>
{currentStep > 0 && ( {currentStep > 0 && (
@@ -1010,6 +1074,7 @@ export default function AddMedicationScreen() {
<Pressable style={styles.pickerBackdrop} onPress={() => setDatePickerVisible(false)} /> <Pressable style={styles.pickerBackdrop} onPress={() => setDatePickerVisible(false)} />
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]} <View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}
> >
<ThemedText style={[styles.modalTitle, { color: colors.text }]}></ThemedText>
<DateTimePicker <DateTimePicker
value={datePickerValue} value={datePickerValue}
mode="date" mode="date"
@@ -1045,6 +1110,51 @@ export default function AddMedicationScreen() {
</View> </View>
</Modal> </Modal>
<Modal
visible={endDatePickerVisible}
transparent
animationType="fade"
onRequestClose={() => setEndDatePickerVisible(false)}
>
<Pressable style={styles.pickerBackdrop} onPress={() => setEndDatePickerVisible(false)} />
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}
>
<ThemedText style={[styles.modalTitle, { color: colors.text }]}></ThemedText>
<DateTimePicker
value={endDatePickerValue}
mode="date"
display={Platform.OS === 'ios' ? 'inline' : 'calendar'}
onChange={(event, date) => {
if (Platform.OS === 'ios') {
if (date) setEndDatePickerValue(date);
} else {
if (event.type === 'set' && date) {
confirmEndDate(date);
} else {
setEndDatePickerVisible(false);
}
}
}}
/>
{Platform.OS === 'ios' && (
<View style={styles.modalActions}>
<Pressable
onPress={() => setEndDatePickerVisible(false)}
style={[styles.modalBtn, { borderColor: softBorderColor }]}
>
<ThemedText style={[styles.modalBtnText, { color: colors.textSecondary }]}></ThemedText>
</Pressable>
<Pressable
onPress={() => confirmEndDate(endDatePickerValue)}
style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colors.primary }]}
>
<ThemedText style={[styles.modalBtnText, { color: colors.onPrimary }]}></ThemedText>
</Pressable>
</View>
)}
</View>
</Modal>
<Modal <Modal
visible={timePickerVisible} visible={timePickerVisible}
transparent transparent
@@ -1500,6 +1610,39 @@ const styles = StyleSheet.create({
footer: { footer: {
gap: 12, 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: { startDateRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',

View File

@@ -25,7 +25,6 @@ export const InfoCard: React.FC<InfoCardProps> = ({
return ( return (
<View style={[ <View style={[
styles.infoCardIcon, styles.infoCardIcon,
clickable && styles.clickableIconFallback
]}> ]}>
<Ionicons name={icon} size={16} color="#4C6EF5" /> <Ionicons name={icon} size={16} color="#4C6EF5" />
</View> </View>
@@ -104,21 +103,16 @@ const styles = StyleSheet.create({
width: 28, width: 28,
height: 28, height: 28,
borderRadius: 14, borderRadius: 14,
backgroundColor: '#EEF1FF',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
clickableIconFallback: {
borderWidth: 1,
borderColor: 'rgba(76, 110, 245, 0.3)',
},
infoCardLabel: { infoCardLabel: {
fontSize: 13, fontSize: 13,
color: '#6B7280', color: '#6B7280',
marginTop: 8, marginTop: 8,
}, },
infoCardValue: { infoCardValue: {
fontSize: 16, fontSize: 14,
fontWeight: '600', fontWeight: '600',
color: '#1F2933', color: '#1F2933',
}, },