feat(medications): 添加AI智能识别药品功能和有效期管理
- 新增AI药品识别流程,支持多角度拍摄和实时进度显示 - 添加药品有效期字段,支持在添加和编辑药品时设置有效期 - 新增MedicationAddOptionsSheet选择录入方式(AI识别/手动录入) - 新增ai-camera和ai-progress两个独立页面处理AI识别流程 - 新增ExpiryDatePickerModal和MedicationPhotoGuideModal组件 - 移除本地通知系统,迁移到服务端推送通知 - 添加medicationNotificationCleanup服务清理旧的本地通知 - 更新药品详情页支持AI草稿模式和有效期显示 - 优化药品表单,支持有效期选择和AI识别结果确认 - 更新i18n资源,添加有效期相关翻译 BREAKING CHANGE: 药品通知系统从本地通知迁移到服务端推送,旧版本的本地通知将被清理
This commit is contained in:
@@ -7,7 +7,6 @@ import { useAppDispatch } from '@/hooks/redux';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||
import { medicationNotificationService } from '@/services/medicationNotifications';
|
||||
import { createMedicationAction, fetchMedicationRecords, fetchMedications } from '@/store/medicationsSlice';
|
||||
import type { MedicationForm, RepeatPattern } from '@/types/medication';
|
||||
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
@@ -151,10 +150,13 @@ export default function AddMedicationScreen() {
|
||||
const [timesPickerValue, setTimesPickerValue] = useState(1);
|
||||
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||
const [expiryDate, setExpiryDate] = 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 [expiryDatePickerVisible, setExpiryDatePickerVisible] = useState(false);
|
||||
const [expiryDatePickerValue, setExpiryDatePickerValue] = 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);
|
||||
@@ -319,6 +321,7 @@ export default function AddMedicationScreen() {
|
||||
medicationTimes: medicationTimes,
|
||||
startDate: dayjs(startDate).startOf('day').toISOString(), // ISO 8601 格式
|
||||
endDate: endDate ? dayjs(endDate).endOf('day').toISOString() : undefined, // 如果有结束日期,设置为当天结束时间
|
||||
expiryDate: expiryDate ? dayjs(expiryDate).endOf('day').toISOString() : undefined, // 如果有有效期,设置为当天结束时间
|
||||
repeatPattern: 'daily' as RepeatPattern,
|
||||
note: note.trim() || undefined,
|
||||
};
|
||||
@@ -333,16 +336,6 @@ export default function AddMedicationScreen() {
|
||||
const today = dayjs().format('YYYY-MM-DD');
|
||||
await dispatch(fetchMedicationRecords({ date: today }));
|
||||
|
||||
// 重新安排药品通知
|
||||
try {
|
||||
// 获取最新的药品列表
|
||||
const medications = await dispatch(fetchMedications({ isActive: true })).unwrap();
|
||||
await medicationNotificationService.rescheduleAllMedicationNotifications(medications);
|
||||
} catch (error) {
|
||||
console.error('[MEDICATION] 安排药品通知失败:', error);
|
||||
// 不影响添加药品的成功流程,只记录错误
|
||||
}
|
||||
|
||||
// 成功提示
|
||||
Alert.alert(
|
||||
'添加成功',
|
||||
@@ -531,6 +524,11 @@ export default function AddMedicationScreen() {
|
||||
setEndDatePickerVisible(true);
|
||||
}, [endDate]);
|
||||
|
||||
const openExpiryDatePicker = useCallback(() => {
|
||||
setExpiryDatePickerValue(expiryDate || new Date());
|
||||
setExpiryDatePickerVisible(true);
|
||||
}, [expiryDate]);
|
||||
|
||||
const confirmStartDate = useCallback((date: Date) => {
|
||||
// 验证开始日期不能早于今天
|
||||
const today = new Date();
|
||||
@@ -563,6 +561,22 @@ export default function AddMedicationScreen() {
|
||||
setEndDatePickerVisible(false);
|
||||
}, [startDate]);
|
||||
|
||||
const confirmExpiryDate = 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;
|
||||
}
|
||||
|
||||
setExpiryDate(date);
|
||||
setExpiryDatePickerVisible(false);
|
||||
}, []);
|
||||
|
||||
const openTimePicker = useCallback(
|
||||
(index?: number) => {
|
||||
try {
|
||||
@@ -872,6 +886,32 @@ export default function AddMedicationScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputGroup}>
|
||||
<View style={styles.periodHeader}>
|
||||
<ThemedText style={[styles.groupLabel, { color: colors.textSecondary }]}>药品有效期</ThemedText>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.85}
|
||||
style={[
|
||||
styles.dateRow,
|
||||
{
|
||||
borderColor: softBorderColor,
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
]}
|
||||
onPress={openExpiryDatePicker}
|
||||
>
|
||||
<View style={styles.dateLeft}>
|
||||
<Ionicons name="time-outline" size={16} color={colors.textSecondary} />
|
||||
<ThemedText style={[styles.dateLabel, { color: colors.textMuted }]}>有效期至</ThemedText>
|
||||
<ThemedText style={[styles.dateValue, { color: colors.text }]}>
|
||||
{expiryDate ? dayjs(expiryDate).format('YYYY/MM/DD') : '未设置'}
|
||||
</ThemedText>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={16} color={colors.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
case 3:
|
||||
@@ -1166,6 +1206,51 @@ export default function AddMedicationScreen() {
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
visible={expiryDatePickerVisible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setExpiryDatePickerVisible(false)}
|
||||
>
|
||||
<Pressable style={styles.pickerBackdrop} onPress={() => setExpiryDatePickerVisible(false)} />
|
||||
<View style={[styles.pickerSheet, { backgroundColor: colors.surface }]}
|
||||
>
|
||||
<ThemedText style={[styles.modalTitle, { color: colors.text }]}>选择药品有效期</ThemedText>
|
||||
<DateTimePicker
|
||||
value={expiryDatePickerValue}
|
||||
mode="date"
|
||||
display={Platform.OS === 'ios' ? 'inline' : 'calendar'}
|
||||
onChange={(event, date) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
if (date) setExpiryDatePickerValue(date);
|
||||
} else {
|
||||
if (event.type === 'set' && date) {
|
||||
confirmExpiryDate(date);
|
||||
} else {
|
||||
setExpiryDatePickerVisible(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{Platform.OS === 'ios' && (
|
||||
<View style={styles.modalActions}>
|
||||
<Pressable
|
||||
onPress={() => setExpiryDatePickerVisible(false)}
|
||||
style={[styles.modalBtn, { borderColor: softBorderColor }]}
|
||||
>
|
||||
<ThemedText style={[styles.modalBtnText, { color: colors.textSecondary }]}>取消</ThemedText>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => confirmExpiryDate(expiryDatePickerValue)}
|
||||
style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colors.primary }]}
|
||||
>
|
||||
<ThemedText style={[styles.modalBtnText, { color: colors.onPrimary }]}>确定</ThemedText>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
visible={endDatePickerVisible}
|
||||
transparent
|
||||
|
||||
Reference in New Issue
Block a user