- 新增AI药品识别流程,支持多角度拍摄和实时进度显示 - 添加药品有效期字段,支持在添加和编辑药品时设置有效期 - 新增MedicationAddOptionsSheet选择录入方式(AI识别/手动录入) - 新增ai-camera和ai-progress两个独立页面处理AI识别流程 - 新增ExpiryDatePickerModal和MedicationPhotoGuideModal组件 - 移除本地通知系统,迁移到服务端推送通知 - 添加medicationNotificationCleanup服务清理旧的本地通知 - 更新药品详情页支持AI草稿模式和有效期显示 - 优化药品表单,支持有效期选择和AI识别结果确认 - 更新i18n资源,添加有效期相关翻译 BREAKING CHANGE: 药品通知系统从本地通知迁移到服务端推送,旧版本的本地通知将被清理
205 lines
5.3 KiB
TypeScript
205 lines
5.3 KiB
TypeScript
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',
|
||
},
|
||
}); |