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
? 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
? medication.medicationTimes.join('、')
: '尚未设置';
@@ -467,8 +484,20 @@ export default function MedicationDetailScreen() {
}, [medication?.photoUrl]);
const handleStartDatePress = useCallback(() => {
Alert.alert('开始日期', `开始服药日期:${startDateLabel}`);
}, [startDateLabel]);
if (!medication) return;
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(() => {
Alert.alert('服药时间', `设置的时间:${reminderTimes}`);
@@ -676,15 +705,15 @@ export default function MedicationDetailScreen() {
<Section title="服药计划" color={colors.text}>
<View style={styles.row}>
<InfoCard
label="开始日期"
value={startDateLabel}
label="服药周期"
value={medicationPeriodLabel}
icon="calendar-outline"
colors={colors}
clickable={false}
onPress={handleStartDatePress}
/>
<InfoCard
label="时间"
label="用药时间"
value={reminderTimes}
icon="time-outline"
colors={colors}

View File

@@ -128,7 +128,7 @@ export default function AddMedicationScreen() {
const { upload, uploading } = useCosUpload({ prefix: 'images/medications' });
// 获取登录验证相关的功能
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 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]);
@@ -146,8 +146,12 @@ export default function AddMedicationScreen() {
const [timesPickerVisible, setTimesPickerVisible] = useState(false);
const [timesPickerValue, setTimesPickerValue] = useState(1);
const [startDate, setStartDate] = useState<Date>(new Date());
const [endDate, setEndDate] = useState<Date | null>(null);
const [datePickerVisible, setDatePickerVisible] = useState(false);
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 [timePickerVisible, setTimePickerVisible] = useState(false);
const [timePickerDate, setTimePickerDate] = useState<Date>(createDateFromTime(DEFAULT_TIME_PRESETS[0]));
@@ -276,6 +280,7 @@ export default function AddMedicationScreen() {
timesPerDay: timesPerDay,
medicationTimes: medicationTimes,
startDate: dayjs(startDate).startOf('day').toISOString(), // ISO 8601 格式
endDate: endDate ? dayjs(endDate).endOf('day').toISOString() : undefined, // 如果有结束日期,设置为当天结束时间
repeatPattern: 'daily' as RepeatPattern,
note: note.trim() || undefined,
};
@@ -332,6 +337,7 @@ export default function AddMedicationScreen() {
dosageUnit,
medicationTimes,
startDate,
endDate,
note,
dispatch,
ensureLoggedIn,
@@ -469,15 +475,47 @@ export default function AddMedicationScreen() {
setPhotoUrl(null);
}, []);
const openDatePicker = useCallback(() => {
const openStartDatePicker = useCallback(() => {
setDatePickerValue(startDate);
setDatePickerVisible(true);
}, [startDate]);
const openEndDatePicker = useCallback(() => {
setEndDatePickerValue(endDate || new Date());
setEndDatePickerVisible(true);
}, [endDate]);
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 openTimePicker = useCallback(
(index?: number) => {
@@ -718,7 +756,6 @@ export default function AddMedicationScreen() {
case 2:
return (
<View style={styles.stepSection}>
<View style={styles.inputGroup}>
<ThemedText style={[styles.groupLabel, { color: colors.textSecondary }]}></ThemedText>
<TouchableOpacity
@@ -736,6 +773,57 @@ export default function AddMedicationScreen() {
<Ionicons name="chevron-down" size={18} color={colors.textSecondary} />
</TouchableOpacity>
</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>
);
case 3:
@@ -895,30 +983,6 @@ export default function AddMedicationScreen() {
<View style={styles.contentContainer}>{renderStepContent()}</View>
<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}>
{currentStep > 0 && (
@@ -1010,6 +1074,7 @@ export default function AddMedicationScreen() {
<Pressable style={styles.pickerBackdrop} onPress={() => setDatePickerVisible(false)} />
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}
>
<ThemedText style={[styles.modalTitle, { color: colors.text }]}></ThemedText>
<DateTimePicker
value={datePickerValue}
mode="date"
@@ -1045,6 +1110,51 @@ export default function AddMedicationScreen() {
</View>
</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
visible={timePickerVisible}
transparent
@@ -1500,6 +1610,39 @@ const styles = StyleSheet.create({
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',

View File

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