diff --git a/app/(tabs)/medications.tsx b/app/(tabs)/medications.tsx index ad346eb..434bbb6 100644 --- a/app/(tabs)/medications.tsx +++ b/app/(tabs)/medications.tsx @@ -64,7 +64,13 @@ export default function MedicationsScreen() { // 从 Redux 获取数据 const selectedKey = selectedDate.format('YYYY-MM-DD'); - const medicationsForDay = useAppSelector((state) => selectMedicationDisplayItemsByDate(selectedKey)(state)); + + // 使用 useMemo 缓存 selector 实例,避免每次渲染都创建新的 selector + const medicationSelector = useMemo( + () => selectMedicationDisplayItemsByDate(selectedKey), + [selectedKey] + ); + const medicationsForDay = useAppSelector(medicationSelector); const handleOpenAddSheet = useCallback(() => { setAddSheetVisible(true); diff --git a/app/_layout.tsx b/app/_layout.tsx index 90dfbc0..bb8b700 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -444,8 +444,8 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { // 2. 开发环境调试工具 if (__DEV__ && BackgroundTaskDebugger) { - BackgroundTaskDebugger.getInstance().initialize(); - logger.info('✅ 后台任务调试工具已初始化(开发环境)'); + logger.info('✅ 后台任务调试工具未初始化(开发环境)'); + return } logger.info('🎉 空闲服务初始化完成'); @@ -525,19 +525,13 @@ export default function RootLayout() { - - - - - - { - console.log('收到通知:', notification); - // 可以在这里处理通知接收逻辑 -}); -``` - -### 通知点击处理 -```typescript -Notifications.addNotificationResponseReceivedListener((response) => { - const { notification } = response; - const data = notification.request.content.data; - - // 根据通知类型处理不同的逻辑 - if (data?.type === 'workout_reminder') { - // 跳转到运动页面 - } else if (data?.type === 'goal_achievement') { - // 跳转到目标页面 - } -}); -``` - -## 最佳实践 - -### 1. 通知内容 -- 标题简洁明了,不超过50个字符 -- 内容具体有用,不超过200个字符 -- 使用适当的优先级和声音 - -### 2. 定时策略 -- 避免过于频繁的通知 -- 考虑用户的使用习惯 -- 提供通知频率设置选项 - -### 3. 错误处理 -- 始终处理权限请求失败的情况 -- 提供用户友好的错误提示 -- 记录通知发送失败的原因 - -### 4. 性能优化 -- 避免同时发送大量通知 -- 及时清理不需要的通知 -- 合理使用重复通知 - -## 测试建议 - -### 1. 功能测试 -- 测试所有通知类型 -- 验证权限请求流程 -- 检查通知点击处理 - -### 2. 兼容性测试 -- 测试不同 iOS 版本 -- 测试不同 Android 版本 -- 验证后台通知功能 - -### 3. 用户体验测试 -- 测试通知时机是否合适 -- 验证通知内容是否清晰 -- 检查通知频率是否合理 - -## 故障排除 - -### 常见问题 - -1. **通知不显示** - - 检查权限是否已授予 - - 确认应用是否在前台 - - 验证通知配置是否正确 - -2. **定时通知不触发** - - 检查设备是否重启 - - 确认应用是否被系统杀死 - - 验证时间设置是否正确 - -3. **权限被拒绝** - - 引导用户到系统设置 - - 提供权限说明 - - 实现降级处理方案 - -### 调试技巧 - -```typescript -// 启用详细日志 -console.log('通知权限状态:', await notificationService.getPermissionStatus()); -console.log('已安排通知:', await notificationService.getAllScheduledNotifications()); - -// 测试通知发送 -await notificationService.sendImmediateNotification({ - title: '测试通知', - body: '这是一个测试通知', - sound: true -}); -``` - -## 总结 - -本推送通知功能实现完整、功能丰富,支持多种通知类型和场景。通过合理的架构设计和错误处理,确保了功能的稳定性和用户体验。开发者可以根据具体需求灵活使用各种通知功能,为用户提供个性化的提醒服务。 diff --git a/services/pushNotificationManager.ts b/services/pushNotificationManager.ts index c1def89..7967bcf 100644 --- a/services/pushNotificationManager.ts +++ b/services/pushNotificationManager.ts @@ -90,9 +90,6 @@ export class PushNotificationManager { // 检查是否需要注册令牌 await this.checkAndRegisterToken(token); - // 设置令牌刷新监听器 - this.setupTokenRefreshListener(); - this.isInitialized = true; console.log('推送通知管理器初始化成功'); return true; @@ -313,16 +310,6 @@ export class PushNotificationManager { } } - /** - * 设置令牌刷新监听器 - */ - private setupTokenRefreshListener(): void { - // 监听令牌变化(iOS上通常不会频繁变化) - Notifications.addNotificationResponseReceivedListener((response) => { - console.log('收到推送通知响应:', response); - }); - } - /** * 获取当前设备令牌 */ diff --git a/store/medicationsSlice.ts b/store/medicationsSlice.ts index 1365946..fa8154c 100644 --- a/store/medicationsSlice.ts +++ b/store/medicationsSlice.ts @@ -11,7 +11,7 @@ import type { MedicationStatus, } from '@/types/medication'; import { convertMedicationDataToWidget, syncMedicationDataToWidget } from '@/utils/widgetDataSync'; -import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { createAsyncThunk, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; import dayjs from 'dayjs'; import type { RootState } from './index'; @@ -695,6 +695,9 @@ export const { // ==================== Selectors ==================== +// 空数组常量,避免每次都创建新数组 +const EMPTY_RECORDS_ARRAY: MedicationRecord[] = []; + export const selectMedicationsState = (state: RootState) => state.medications; export const selectMedications = (state: RootState) => state.medications.medications; export const selectActiveMedications = (state: RootState) => @@ -708,7 +711,7 @@ export const selectOverallStats = (state: RootState) => state.medications.overal * 获取指定日期的服药记录 */ export const selectMedicationRecordsByDate = (date: string) => (state: RootState) => { - return state.medications.medicationRecords[date] || []; + return state.medications.medicationRecords[date] || EMPTY_RECORDS_ARRAY; }; /** @@ -716,7 +719,7 @@ export const selectMedicationRecordsByDate = (date: string) => (state: RootState */ export const selectSelectedDateMedicationRecords = (state: RootState) => { const selectedDate = state.medications.selectedDate; - return state.medications.medicationRecords[selectedDate] || []; + return state.medications.medicationRecords[selectedDate] || EMPTY_RECORDS_ARRAY; }; /** @@ -735,72 +738,76 @@ export const selectSelectedDateStats = (state: RootState) => { }; /** - * 获取指定日期的展示项列表(用于UI渲染) + * 获取指定日期的展示项列表(用于UI渲染)- 使用 createSelector 进行 memoization * 将药物记录和药物信息合并为展示项 * 排序规则:优先显示未服用的药品(upcoming、missed),然后是已服用的药品(taken、skipped) */ -export const selectMedicationDisplayItemsByDate = (date: string) => (state: RootState) => { - const records = state.medications.medicationRecords[date] || []; - const medications = state.medications.medications; - - // 创建药物ID到药物的映射 - const medicationMap = new Map(); - medications.forEach((med) => medicationMap.set(med.id, med)); - - // 转换为展示项 - const displayItems = records - .map((record) => { - const medication = medicationMap.get(record.medicationId); - if (!medication) return null; +export const selectMedicationDisplayItemsByDate = (date: string) => + createSelector( + [ + (state: RootState) => state.medications.medicationRecords[date] || EMPTY_RECORDS_ARRAY, + (state: RootState) => state.medications.medications, + ], + (records, medications) => { + // 创建药物ID到药物的映射 + const medicationMap = new Map(); + medications.forEach((med) => medicationMap.set(med.id, med)); - // 格式化剂量 - const dosage = `${medication.dosageValue} ${medication.dosageUnit}`; + // 转换为展示项 + const displayItems = records + .map((record) => { + const medication = medicationMap.get(record.medicationId); + if (!medication) return null; + + // 格式化剂量 + const dosage = `${medication.dosageValue} ${medication.dosageUnit}`; + + // 提取并格式化为当地时间(HH:mm格式) + // 服务端返回的是UTC时间,需要转换为用户本地时间显示 + const localTime = dayjs(record.scheduledTime).format('HH:mm'); + const scheduledTime = localTime || '00:00'; + + // 频率描述 + const frequency = medication.repeatPattern === 'daily' ? '每日' : '自定义'; + + return { + id: record.id, + name: medication.name, + dosage, + scheduledTime, + frequency, + status: record.status, + recordId: record.id, + medicationId: medication.id, + image: medication.photoUrl ? { uri: medication.photoUrl } : undefined + } as import('@/types/medication').MedicationDisplayItem; + }) + .filter((item): item is import('@/types/medication').MedicationDisplayItem => item !== null); - // 提取并格式化为当地时间(HH:mm格式) - // 服务端返回的是UTC时间,需要转换为用户本地时间显示 - const localTime = dayjs(record.scheduledTime).format('HH:mm'); - const scheduledTime = localTime || '00:00'; - - // 频率描述 - const frequency = medication.repeatPattern === 'daily' ? '每日' : '自定义'; - - return { - id: record.id, - name: medication.name, - dosage, - scheduledTime, - frequency, - status: record.status, - recordId: record.id, - medicationId: medication.id, - image: medication.photoUrl ? { uri: medication.photoUrl } : undefined - } as import('@/types/medication').MedicationDisplayItem; - }) - .filter((item): item is import('@/types/medication').MedicationDisplayItem => item !== null); - - // 排序:未服用的药品(upcoming、missed)优先,已服用的药品(taken、skipped)其次 - // 在同一组内,按计划时间升序排列 - return displayItems.sort((a, b) => { - // 定义状态优先级:数值越小优先级越高 - const statusPriority: Record = { - 'missed': 1, // 已错过 - 最高优先级 - 'upcoming': 2, // 待服用 - 'taken': 3, // 已服用 - 'skipped': 4, // 已跳过 - }; - - const priorityA = statusPriority[a.status]; - const priorityB = statusPriority[b.status]; - - // 首先按状态优先级排序 - if (priorityA !== priorityB) { - return priorityA - priorityB; + // 排序:未服用的药品(upcoming、missed)优先,已服用的药品(taken、skipped)其次 + // 在同一组内,按计划时间升序排列 + return displayItems.sort((a, b) => { + // 定义状态优先级:数值越小优先级越高 + const statusPriority: Record = { + 'missed': 1, // 已错过 - 最高优先级 + 'upcoming': 2, // 待服用 + 'taken': 3, // 已服用 + 'skipped': 4, // 已跳过 + }; + + const priorityA = statusPriority[a.status]; + const priorityB = statusPriority[b.status]; + + // 首先按状态优先级排序 + if (priorityA !== priorityB) { + return priorityA - priorityB; + } + + // 状态相同时,按计划时间升序排列 + return a.scheduledTime.localeCompare(b.scheduledTime); + }); } - - // 状态相同时,按计划时间升序排列 - return a.scheduledTime.localeCompare(b.scheduledTime); - }); -}; + ); // ==================== Export ==================== diff --git a/store/tabBarConfigSlice.ts b/store/tabBarConfigSlice.ts index 4a3f166..8ffa78f 100644 --- a/store/tabBarConfigSlice.ts +++ b/store/tabBarConfigSlice.ts @@ -1,6 +1,6 @@ import AsyncStorage from '@/utils/kvStore'; import { logger } from '@/utils/logger'; -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './index'; // Tab 配置接口 @@ -190,10 +190,15 @@ export const { // Selectors export const selectTabBarConfigs = (state: RootState) => state.tabBarConfig.configs; -export const selectEnabledTabs = (state: RootState) => - state.tabBarConfig.configs + +// ✅ 使用 createSelector 进行记忆化,避免不必要的重渲染 +export const selectEnabledTabs = createSelector( + [selectTabBarConfigs], + (configs) => configs .filter(config => config.enabled) - .sort((a, b) => a.order - b.order); + .sort((a, b) => a.order - b.order) +); + export const selectIsInitialized = (state: RootState) => state.tabBarConfig.isInitialized; // 按 id 获取配置