Files
digital-pilates/components/medications/ExpiryDatePickerModal.tsx
richarjiang bcb910140e feat(medications): 添加AI智能识别药品功能和有效期管理
- 新增AI药品识别流程,支持多角度拍摄和实时进度显示
- 添加药品有效期字段,支持在添加和编辑药品时设置有效期
- 新增MedicationAddOptionsSheet选择录入方式(AI识别/手动录入)
- 新增ai-camera和ai-progress两个独立页面处理AI识别流程
- 新增ExpiryDatePickerModal和MedicationPhotoGuideModal组件
- 移除本地通知系统,迁移到服务端推送通知
- 添加medicationNotificationCleanup服务清理旧的本地通知
- 更新药品详情页支持AI草稿模式和有效期显示
- 优化药品表单,支持有效期选择和AI识别结果确认
- 更新i18n资源,添加有效期相关翻译

BREAKING CHANGE: 药品通知系统从本地通知迁移到服务端推送,旧版本的本地通知将被清理
2025-11-21 17:32:44 +08:00

205 lines
5.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ThemedText } from '@/components/ThemedText';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useI18n } from '@/hooks/useI18n';
import DateTimePicker from '@react-native-community/datetimepicker';
import dayjs from 'dayjs';
import React, { useCallback, useEffect, useState } from 'react';
import { Alert, Modal, Platform, Pressable, StyleSheet, View } from 'react-native';
interface ExpiryDatePickerModalProps {
visible: boolean;
currentDate: Date | null;
onClose: () => void;
onConfirm: (date: Date) => void;
isAiDraft?: boolean;
}
/**
* 有效期日期选择器组件
*
* 功能:
* - 显示日期选择器弹窗
* - 验证日期不能早于今天
* - iOS 显示内联日历Android 显示原生对话框
* - 支持取消和确认操作
*/
export function ExpiryDatePickerModal({
visible,
currentDate,
onClose,
onConfirm,
isAiDraft = false,
}: ExpiryDatePickerModalProps) {
const { t } = useI18n();
const scheme = (useColorScheme() ?? 'light') as keyof typeof Colors;
const colors = Colors[scheme];
// 内部状态:选择的日期值
const [selectedDate, setSelectedDate] = useState<Date>(currentDate || new Date());
// 当弹窗显示时,同步当前日期
useEffect(() => {
if (visible) {
setSelectedDate(currentDate || new Date());
}
}, [visible, currentDate]);
/**
* 处理日期变化
* iOS: 实时更新选择的日期
* Android: 在用户点击确定时直接确认
*/
const handleDateChange = useCallback(
(event: any, date?: Date) => {
if (Platform.OS === 'ios') {
// iOS: 实时更新内部状态
if (date) {
setSelectedDate(date);
}
} else {
// Android: 处理用户操作
if (event.type === 'set' && date) {
// 用户点击确定
validateAndConfirm(date);
} else {
// 用户点击取消
onClose();
}
}
},
[onClose]
);
/**
* 验证并确认日期
*/
const validateAndConfirm = useCallback(
(dateToConfirm: Date) => {
// 验证有效期不能早于今天
const today = new Date();
today.setHours(0, 0, 0, 0);
const selected = new Date(dateToConfirm);
selected.setHours(0, 0, 0, 0);
if (selected < today) {
Alert.alert('日期无效', '有效期不能早于今天');
return;
}
// 检查日期是否真的发生了变化
const currentExpiry = currentDate ? dayjs(currentDate).format('YYYY-MM-DD') : null;
const newExpiry = dayjs(dateToConfirm).format('YYYY-MM-DD');
if (currentExpiry === newExpiry) {
// 日期没有变化,直接关闭
onClose();
return;
}
// 日期有效且发生了变化,执行确认回调
onConfirm(dateToConfirm);
onClose();
},
[currentDate, onClose, onConfirm]
);
/**
* iOS 平台的确认按钮处理
*/
const handleIOSConfirm = useCallback(() => {
validateAndConfirm(selectedDate);
}, [selectedDate, validateAndConfirm]);
return (
<Modal
visible={visible}
transparent
animationType="fade"
onRequestClose={onClose}
>
<Pressable style={styles.backdrop} onPress={onClose} />
<View style={[styles.sheet, { backgroundColor: colors.surface }]}>
<ThemedText style={[styles.title, { color: colors.text }]}>
</ThemedText>
<DateTimePicker
value={selectedDate}
mode="date"
display={Platform.OS === 'ios' ? 'inline' : 'calendar'}
minimumDate={new Date()}
onChange={handleDateChange}
locale="zh-CN"
/>
{/* iOS 平台显示确认和取消按钮 */}
{Platform.OS === 'ios' && (
<View style={styles.actions}>
<Pressable
onPress={onClose}
style={[styles.btn, { borderColor: colors.border }]}
>
<ThemedText style={[styles.btnText, { color: colors.textSecondary }]}>
{t('medications.detail.pickers.cancel')}
</ThemedText>
</Pressable>
<Pressable
onPress={handleIOSConfirm}
style={[styles.btn, styles.btnPrimary, { backgroundColor: colors.primary }]}
>
<ThemedText style={[styles.btnText, { color: colors.onPrimary }]}>
{t('medications.detail.pickers.confirm')}
</ThemedText>
</Pressable>
</View>
)}
</View>
</Modal>
);
}
const styles = StyleSheet.create({
backdrop: {
flex: 1,
backgroundColor: 'rgba(15, 23, 42, 0.4)',
},
sheet: {
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,
},
title: {
fontSize: 20,
fontWeight: '700',
marginBottom: 20,
textAlign: 'center',
},
actions: {
flexDirection: 'row',
gap: 12,
marginTop: 16,
},
btn: {
flex: 1,
paddingVertical: 14,
borderRadius: 16,
alignItems: 'center',
borderWidth: 1,
},
btnPrimary: {
borderWidth: 0,
},
btnText: {
fontSize: 16,
fontWeight: '600',
},
});