feat(medications): 重构药品通知系统并添加独立设置页面

- 创建药品通知服务模块,统一管理药品提醒通知的调度和取消
- 新增独立的通知设置页面,支持总开关和药品提醒开关分离控制
- 重构药品详情页面,移除频率编辑功能到独立页面
- 优化药品添加流程,支持拍照和相册选择图片
- 改进通知权限检查和错误处理机制
- 更新用户偏好设置,添加药品提醒开关配置
This commit is contained in:
richarjiang
2025-11-11 16:43:27 +08:00
parent d9975813cb
commit f4ce3d9edf
12 changed files with 1551 additions and 524 deletions

View File

@@ -5,6 +5,7 @@ import { IconSymbol } from '@/components/ui/IconSymbol';
import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { medicationNotificationService } from '@/services/medicationNotifications';
import { fetchMedicationRecords, fetchMedications, selectMedicationDisplayItemsByDate } from '@/store/medicationsSlice';
import { DEFAULT_MEMBER_NAME } from '@/store/userSlice';
import { useFocusEffect } from '@react-navigation/native';
@@ -67,8 +68,23 @@ export default function MedicationsScreen() {
// 页面聚焦时刷新数据,确保从添加页面返回时能看到最新数据
useFocusEffect(
useCallback(() => {
dispatch(fetchMedications({ isActive: true }));
dispatch(fetchMedicationRecords({ date: selectedKey }));
// 重新安排药品通知并刷新数据
const refreshDataAndRescheduleNotifications = async () => {
try {
// 只获取一次药物数据,然后复用结果
const medications = await dispatch(fetchMedications({ isActive: true })).unwrap();
// 并行执行获取药物记录和安排通知
await Promise.all([
dispatch(fetchMedicationRecords({ date: selectedKey })),
medicationNotificationService.rescheduleAllMedicationNotifications(medications),
]);
} catch (error) {
console.error('刷新数据或重新安排药品通知失败:', error);
}
};
refreshDataAndRescheduleNotifications();
}, [dispatch, selectedKey])
);
@@ -78,8 +94,6 @@ export default function MedicationsScreen() {
// 为每个药物添加默认图片(如果没有图片)
const medicationsWithImages = useMemo(() => {
console.log('medicationsForDay', medicationsForDay);
return medicationsForDay.map((med: any) => ({
...med,
image: med.image || require('@/assets/images/medicine/image-medicine.png'), // 默认使用瓶子图标

View File

@@ -10,7 +10,6 @@ import { selectActiveMembershipPlanName } from '@/store/membershipSlice';
import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice';
import { getItem, setItem } from '@/utils/kvStore';
import { log } from '@/utils/logger';
import { getNotificationEnabled, setNotificationEnabled as saveNotificationEnabled } from '@/utils/userPreferences';
import { Ionicons } from '@expo/vector-icons';
import { useFocusEffect } from '@react-navigation/native';
import dayjs from 'dayjs';
@@ -18,7 +17,7 @@ import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Alert, Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
import { Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/seal-avatar/2.jpeg';
@@ -37,7 +36,7 @@ export default function PersonalScreen() {
sendNotification,
} = useNotifications();
const [notificationEnabled, setNotificationEnabled] = useState(false);
// 移除 notificationEnabled 状态,因为现在在通知设置页面中管理
// 开发者模式相关状态
const [showDeveloperSection, setShowDeveloperSection] = useState(false);
@@ -67,22 +66,13 @@ export default function PersonalScreen() {
React.useCallback(() => {
dispatch(fetchMyProfile());
dispatch(fetchActivityHistory());
// 加载用户推送偏好设置
loadNotificationPreference();
// 不再需要在这里加载推送偏好设置,因为已移到通知设置页面
// 加载开发者模式状态
loadDeveloperModeState();
}, [dispatch])
);
// 加载用户推送偏好设置
const loadNotificationPreference = async () => {
try {
const enabled = await getNotificationEnabled();
setNotificationEnabled(enabled);
} catch (error) {
console.error('加载推送偏好设置失败:', error);
}
};
// 移除 loadNotificationPreference 函数,因为已移到通知设置页面
// 加载开发者模式状态
const loadDeveloperModeState = async () => {
@@ -127,9 +117,8 @@ export default function PersonalScreen() {
// 显示名称
const displayName = (userProfile.name?.trim()) ? userProfile.name : DEFAULT_MEMBER_NAME;
// 初始化时加载推送偏好设置和开发者模式状态
// 初始化时加载开发者模式状态
useEffect(() => {
loadNotificationPreference();
loadDeveloperModeState();
}, []);
@@ -160,50 +149,7 @@ export default function PersonalScreen() {
}
};
// 处理通知开关变化
const handleNotificationToggle = async (value: boolean) => {
if (value) {
try {
// 先检查系统权限
const status = await requestPermission();
if (status === 'granted') {
// 系统权限获取成功,保存用户偏好设置
await saveNotificationEnabled(true);
setNotificationEnabled(true);
// 发送测试通知
await sendNotification({
title: '通知已开启',
body: '您将收到运动提醒和重要通知',
sound: true,
priority: 'normal',
});
} else {
// 系统权限被拒绝,不更新用户偏好设置
Alert.alert(
'权限被拒绝',
'请在系统设置中开启通知权限,然后再尝试开启推送功能',
[
{ text: '取消', style: 'cancel' },
{ text: '去设置', onPress: () => Linking.openSettings() }
]
);
}
} catch (error) {
console.error('开启推送通知失败:', error);
Alert.alert('错误', '请求通知权限失败');
}
} else {
try {
// 关闭推送,保存用户偏好设置
await saveNotificationEnabled(false);
setNotificationEnabled(false);
} catch (error) {
console.error('关闭推送通知失败:', error);
Alert.alert('错误', '保存设置失败');
}
}
};
// 移除 handleNotificationToggle 函数,因为已移到通知设置页面
// 用户信息头部
const UserHeader = () => (
@@ -412,10 +358,8 @@ export default function PersonalScreen() {
items: [
{
icon: 'notifications-outline' as const,
title: '消息推送',
type: 'switch' as const,
switchValue: notificationEnabled,
onSwitchChange: handleNotificationToggle,
title: '通知设置',
onPress: () => pushIfAuthedElseLogin(ROUTES.NOTIFICATION_SETTINGS),
}
],
},