feat(medications): 添加AI智能识别药品功能和有效期管理
- 新增AI药品识别流程,支持多角度拍摄和实时进度显示 - 添加药品有效期字段,支持在添加和编辑药品时设置有效期 - 新增MedicationAddOptionsSheet选择录入方式(AI识别/手动录入) - 新增ai-camera和ai-progress两个独立页面处理AI识别流程 - 新增ExpiryDatePickerModal和MedicationPhotoGuideModal组件 - 移除本地通知系统,迁移到服务端推送通知 - 添加medicationNotificationCleanup服务清理旧的本地通知 - 更新药品详情页支持AI草稿模式和有效期显示 - 优化药品表单,支持有效期选择和AI识别结果确认 - 更新i18n资源,添加有效期相关翻译 BREAKING CHANGE: 药品通知系统从本地通知迁移到服务端推送,旧版本的本地通知将被清理
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
import CelebrationAnimation, { CelebrationAnimationRef } from '@/components/CelebrationAnimation';
|
||||
import { DateSelector } from '@/components/DateSelector';
|
||||
import { MedicationAddOptionsSheet } from '@/components/medication/MedicationAddOptionsSheet';
|
||||
import { MedicationCard } from '@/components/medication/MedicationCard';
|
||||
import { TakenMedicationsStack } from '@/components/medication/TakenMedicationsStack';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import { MedicalDisclaimerSheet } from '@/components/ui/MedicalDisclaimerSheet';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useMembershipModal } from '@/contexts/MembershipModalContext';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { medicationNotificationService } from '@/services/medicationNotifications';
|
||||
import { useVipService } from '@/hooks/useVipService';
|
||||
import { fetchMedicationRecords, fetchMedications, selectMedicationDisplayItemsByDate } from '@/store/medicationsSlice';
|
||||
import { DEFAULT_MEMBER_NAME } from '@/store/userSlice';
|
||||
import { getItemSync, setItemSync } from '@/utils/kvStore';
|
||||
@@ -46,6 +49,9 @@ export default function MedicationsScreen() {
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colors: ThemeColors = Colors[theme];
|
||||
const userProfile = useAppSelector((state) => state.user.profile);
|
||||
const { ensureLoggedIn } = useAuthGuard();
|
||||
const { checkServiceAccess } = useVipService();
|
||||
const { openMembershipModal } = useMembershipModal();
|
||||
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
|
||||
const [selectedDateIndex, setSelectedDateIndex] = useState<number>(selectedDate.date() - 1);
|
||||
const [activeFilter, setActiveFilter] = useState<MedicationFilter>('all');
|
||||
@@ -53,34 +59,59 @@ export default function MedicationsScreen() {
|
||||
const celebrationTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [isCelebrationVisible, setIsCelebrationVisible] = useState(false);
|
||||
const [disclaimerVisible, setDisclaimerVisible] = useState(false);
|
||||
const [addSheetVisible, setAddSheetVisible] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState<'manual' | null>(null);
|
||||
|
||||
// 从 Redux 获取数据
|
||||
const selectedKey = selectedDate.format('YYYY-MM-DD');
|
||||
const medicationsForDay = useAppSelector((state) => selectMedicationDisplayItemsByDate(selectedKey)(state));
|
||||
|
||||
const handleOpenAddMedication = useCallback(() => {
|
||||
// 检查是否已经读过免责声明
|
||||
const handleOpenAddSheet = useCallback(() => {
|
||||
setAddSheetVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleManualAdd = useCallback(() => {
|
||||
const hasRead = getItemSync(MEDICAL_DISCLAIMER_READ_KEY);
|
||||
|
||||
setPendingAction('manual');
|
||||
|
||||
if (hasRead === 'true') {
|
||||
// 已读过,直接跳转
|
||||
setAddSheetVisible(false);
|
||||
setPendingAction(null);
|
||||
router.push('/medications/add-medication');
|
||||
} else {
|
||||
// 未读过,显示医疗免责声明弹窗
|
||||
setAddSheetVisible(false);
|
||||
setDisclaimerVisible(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAiRecognize = useCallback(async () => {
|
||||
setAddSheetVisible(false);
|
||||
const isLoggedIn = await ensureLoggedIn();
|
||||
if (!isLoggedIn) return;
|
||||
|
||||
const access = checkServiceAccess();
|
||||
if (!access.canUseService) {
|
||||
openMembershipModal();
|
||||
return;
|
||||
}
|
||||
|
||||
router.push('/medications/ai-camera');
|
||||
}, [checkServiceAccess, ensureLoggedIn, openMembershipModal, router]);
|
||||
|
||||
const handleDisclaimerConfirm = useCallback(() => {
|
||||
// 用户同意免责声明后,记录已读状态,关闭弹窗并跳转到添加页面
|
||||
setItemSync(MEDICAL_DISCLAIMER_READ_KEY, 'true');
|
||||
setDisclaimerVisible(false);
|
||||
router.push('/medications/add-medication');
|
||||
}, []);
|
||||
if (pendingAction === 'manual') {
|
||||
setPendingAction(null);
|
||||
router.push('/medications/add-medication');
|
||||
}
|
||||
}, [pendingAction]);
|
||||
|
||||
const handleDisclaimerClose = useCallback(() => {
|
||||
// 用户不接受免责声明,只关闭弹窗,不跳转,不记录已读状态
|
||||
setDisclaimerVisible(false);
|
||||
setPendingAction(null);
|
||||
}, []);
|
||||
|
||||
const handleOpenMedicationManagement = useCallback(() => {
|
||||
@@ -133,11 +164,8 @@ export default function MedicationsScreen() {
|
||||
// 只获取一次药物数据,然后复用结果
|
||||
const medications = await dispatch(fetchMedications({ isActive: true })).unwrap();
|
||||
|
||||
// 并行执行获取药物记录和安排通知
|
||||
const [recordsAction] = await Promise.all([
|
||||
dispatch(fetchMedicationRecords({ date: selectedKey })),
|
||||
medicationNotificationService.rescheduleAllMedicationNotifications(medications),
|
||||
]);
|
||||
// 获取药物记录
|
||||
const recordsAction = await dispatch(fetchMedicationRecords({ date: selectedKey }));
|
||||
|
||||
// 同步数据到小组件(仅同步今天的)
|
||||
const today = dayjs().format('YYYY-MM-DD');
|
||||
@@ -274,7 +302,7 @@ export default function MedicationsScreen() {
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={handleOpenAddMedication}
|
||||
onPress={handleOpenAddSheet}
|
||||
>
|
||||
{isLiquidGlassAvailable() ? (
|
||||
<GlassView
|
||||
@@ -391,6 +419,13 @@ export default function MedicationsScreen() {
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<MedicationAddOptionsSheet
|
||||
visible={addSheetVisible}
|
||||
onClose={() => setAddSheetVisible(false)}
|
||||
onManualAdd={handleManualAdd}
|
||||
onAiRecognize={handleAiRecognize}
|
||||
/>
|
||||
|
||||
{/* 医疗免责声明弹窗 */}
|
||||
<MedicalDisclaimerSheet
|
||||
visible={disclaimerVisible}
|
||||
|
||||
Reference in New Issue
Block a user