perf(store): 优化 selector 性能并移除未使用代码
- 使用 createSelector 和 useMemo 优化 medications 和 tabBarConfig 的 selector,避免不必要的重渲染 - 添加空数组常量 EMPTY_RECORDS_ARRAY,减少对象创建开销 - 移除 _layout.tsx 中未使用的路由配置 - 删除过时的通知实现文档 - 移除 pushNotificationManager 中未使用的 token 刷新监听器 - 禁用开发环境的后台任务调试工具初始化
This commit is contained in:
@@ -64,7 +64,13 @@ export default function MedicationsScreen() {
|
|||||||
|
|
||||||
// 从 Redux 获取数据
|
// 从 Redux 获取数据
|
||||||
const selectedKey = selectedDate.format('YYYY-MM-DD');
|
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(() => {
|
const handleOpenAddSheet = useCallback(() => {
|
||||||
setAddSheetVisible(true);
|
setAddSheetVisible(true);
|
||||||
|
|||||||
@@ -444,8 +444,8 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
// 2. 开发环境调试工具
|
// 2. 开发环境调试工具
|
||||||
if (__DEV__ && BackgroundTaskDebugger) {
|
if (__DEV__ && BackgroundTaskDebugger) {
|
||||||
BackgroundTaskDebugger.getInstance().initialize();
|
logger.info('✅ 后台任务调试工具未初始化(开发环境)');
|
||||||
logger.info('✅ 后台任务调试工具已初始化(开发环境)');
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('🎉 空闲服务初始化完成');
|
logger.info('🎉 空闲服务初始化完成');
|
||||||
@@ -525,19 +525,13 @@ export default function RootLayout() {
|
|||||||
<Stack screenOptions={{ headerShown: false }}>
|
<Stack screenOptions={{ headerShown: false }}>
|
||||||
<Stack.Screen name="onboarding" />
|
<Stack.Screen name="onboarding" />
|
||||||
<Stack.Screen name="(tabs)" />
|
<Stack.Screen name="(tabs)" />
|
||||||
<Stack.Screen name="challenge" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="training-plan" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="workout" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="profile/edit" />
|
<Stack.Screen name="profile/edit" />
|
||||||
<Stack.Screen name="fasting/[planId]" options={{ headerShown: false }} />
|
<Stack.Screen name="fasting/[planId]" options={{ headerShown: false }} />
|
||||||
|
|
||||||
<Stack.Screen name="ai-posture-assessment" />
|
|
||||||
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
|
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
||||||
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
||||||
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="water-detail" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="water-settings" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="workout/notification-settings" options={{ headerShown: false }} />
|
<Stack.Screen name="workout/notification-settings" options={{ headerShown: false }} />
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="health-data-permissions"
|
name="health-data-permissions"
|
||||||
|
|||||||
@@ -1,304 +0,0 @@
|
|||||||
# 推送通知功能实现文档
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
本项目已成功集成本地推送通知功能,使用 Expo 官方的 `expo-notifications` 库。该功能支持立即通知、定时通知、重复通知等多种类型,并提供了完整的权限管理和通知处理机制。
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
- **expo-notifications**: Expo 官方推送通知库
|
|
||||||
- **React Native**: 跨平台移动应用框架
|
|
||||||
- **TypeScript**: 类型安全的 JavaScript 超集
|
|
||||||
|
|
||||||
## 文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
services/
|
|
||||||
├── notifications.ts # 推送通知服务核心逻辑
|
|
||||||
hooks/
|
|
||||||
├── useNotifications.ts # 推送通知自定义 Hook
|
|
||||||
components/
|
|
||||||
├── NotificationTest.tsx # 通知功能测试组件
|
|
||||||
app/(tabs)/
|
|
||||||
├── personal.tsx # 个人页面(集成通知开关)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 核心功能
|
|
||||||
|
|
||||||
### 1. 通知服务 (services/notifications.ts)
|
|
||||||
|
|
||||||
#### 主要特性
|
|
||||||
- **单例模式**: 确保全局只有一个通知服务实例
|
|
||||||
- **权限管理**: 自动请求和管理通知权限
|
|
||||||
- **多种通知类型**: 支持立即、定时、重复通知
|
|
||||||
- **通知监听**: 处理通知接收和点击事件
|
|
||||||
- **便捷方法**: 提供常用通知类型的快捷发送方法
|
|
||||||
|
|
||||||
#### 核心方法
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 初始化通知服务
|
|
||||||
await notificationService.initialize();
|
|
||||||
|
|
||||||
// 发送立即通知
|
|
||||||
await notificationService.sendImmediateNotification({
|
|
||||||
title: '标题',
|
|
||||||
body: '内容',
|
|
||||||
sound: true,
|
|
||||||
priority: 'high'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 安排定时通知
|
|
||||||
await notificationService.scheduleNotificationAtDate(
|
|
||||||
notification,
|
|
||||||
new Date(Date.now() + 5000) // 5秒后
|
|
||||||
);
|
|
||||||
|
|
||||||
// 安排重复通知
|
|
||||||
await notificationService.scheduleRepeatingNotification(
|
|
||||||
notification,
|
|
||||||
{ minutes: 1 } // 每分钟重复
|
|
||||||
);
|
|
||||||
|
|
||||||
// 取消通知
|
|
||||||
await notificationService.cancelNotification(notificationId);
|
|
||||||
await notificationService.cancelAllNotifications();
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 自定义 Hook (hooks/useNotifications.ts)
|
|
||||||
|
|
||||||
#### 主要特性
|
|
||||||
- **状态管理**: 管理通知权限和初始化状态
|
|
||||||
- **自动初始化**: 组件挂载时自动初始化通知服务
|
|
||||||
- **便捷接口**: 提供简化的通知操作方法
|
|
||||||
- **类型安全**: 完整的 TypeScript 类型定义
|
|
||||||
|
|
||||||
#### 使用示例
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const {
|
|
||||||
isInitialized,
|
|
||||||
permissionStatus,
|
|
||||||
sendNotification,
|
|
||||||
scheduleNotification,
|
|
||||||
sendWorkoutReminder,
|
|
||||||
sendGoalAchievement,
|
|
||||||
} = useNotifications();
|
|
||||||
|
|
||||||
// 发送运动提醒
|
|
||||||
await sendWorkoutReminder('运动提醒', '该开始今天的普拉提训练了!');
|
|
||||||
|
|
||||||
// 发送目标达成通知
|
|
||||||
await sendGoalAchievement('目标达成', '恭喜您完成了本周的运动目标!');
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 测试组件 (components/NotificationTest.tsx)
|
|
||||||
|
|
||||||
#### 功能特性
|
|
||||||
- **完整测试**: 测试所有通知功能
|
|
||||||
- **状态显示**: 显示初始化状态和权限状态
|
|
||||||
- **交互测试**: 提供各种通知类型的测试按钮
|
|
||||||
- **通知列表**: 显示已安排的通知列表
|
|
||||||
|
|
||||||
## 配置说明
|
|
||||||
|
|
||||||
### app.json 配置
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"expo": {
|
|
||||||
"plugins": [
|
|
||||||
[
|
|
||||||
"expo-notifications",
|
|
||||||
{
|
|
||||||
"icon": "./assets/images/Sealife.jpeg",
|
|
||||||
"color": "#ffffff",
|
|
||||||
"sounds": ["./assets/sounds/notification.wav"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"ios": {
|
|
||||||
"infoPlist": {
|
|
||||||
"UIBackgroundModes": ["remote-notification"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"android": {
|
|
||||||
"permissions": [
|
|
||||||
"android.permission.RECEIVE_BOOT_COMPLETED",
|
|
||||||
"android.permission.VIBRATE",
|
|
||||||
"android.permission.WAKE_LOCK"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用场景
|
|
||||||
|
|
||||||
### 1. 运动提醒
|
|
||||||
```typescript
|
|
||||||
// 每天定时发送运动提醒
|
|
||||||
await scheduleRepeatingNotification(
|
|
||||||
{
|
|
||||||
title: '运动提醒',
|
|
||||||
body: '该开始今天的普拉提训练了!',
|
|
||||||
data: { type: 'workout_reminder' },
|
|
||||||
sound: true,
|
|
||||||
priority: 'high'
|
|
||||||
},
|
|
||||||
{ days: 1 }
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 目标达成通知
|
|
||||||
```typescript
|
|
||||||
// 用户达成目标时立即发送通知
|
|
||||||
await sendGoalAchievement('目标达成', '恭喜您完成了本周的运动目标!');
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 心情打卡提醒
|
|
||||||
```typescript
|
|
||||||
// 每天晚上提醒用户记录心情
|
|
||||||
const eveningTime = new Date();
|
|
||||||
eveningTime.setHours(20, 0, 0, 0);
|
|
||||||
|
|
||||||
await scheduleNotification(
|
|
||||||
{
|
|
||||||
title: '心情打卡',
|
|
||||||
body: '记得记录今天的心情状态哦',
|
|
||||||
data: { type: 'mood_checkin' },
|
|
||||||
sound: true,
|
|
||||||
priority: 'normal'
|
|
||||||
},
|
|
||||||
eveningTime
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 营养提醒
|
|
||||||
```typescript
|
|
||||||
// 定时提醒用户记录饮食
|
|
||||||
await scheduleRepeatingNotification(
|
|
||||||
{
|
|
||||||
title: '营养记录',
|
|
||||||
body: '记得记录今天的饮食情况',
|
|
||||||
data: { type: 'nutrition_reminder' },
|
|
||||||
sound: true,
|
|
||||||
priority: 'normal'
|
|
||||||
},
|
|
||||||
{ hours: 4 } // 每4小时提醒一次
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 权限处理
|
|
||||||
|
|
||||||
### iOS 权限
|
|
||||||
- 自动请求通知权限
|
|
||||||
- 支持后台通知模式
|
|
||||||
- 处理权限被拒绝的情况
|
|
||||||
|
|
||||||
### Android 权限
|
|
||||||
- 自动请求必要权限
|
|
||||||
- 支持开机启动和唤醒锁
|
|
||||||
- 处理权限被拒绝的情况
|
|
||||||
|
|
||||||
## 通知处理
|
|
||||||
|
|
||||||
### 通知接收处理
|
|
||||||
```typescript
|
|
||||||
Notifications.addNotificationReceivedListener((notification) => {
|
|
||||||
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
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
本推送通知功能实现完整、功能丰富,支持多种通知类型和场景。通过合理的架构设计和错误处理,确保了功能的稳定性和用户体验。开发者可以根据具体需求灵活使用各种通知功能,为用户提供个性化的提醒服务。
|
|
||||||
@@ -90,9 +90,6 @@ export class PushNotificationManager {
|
|||||||
// 检查是否需要注册令牌
|
// 检查是否需要注册令牌
|
||||||
await this.checkAndRegisterToken(token);
|
await this.checkAndRegisterToken(token);
|
||||||
|
|
||||||
// 设置令牌刷新监听器
|
|
||||||
this.setupTokenRefreshListener();
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
console.log('推送通知管理器初始化成功');
|
console.log('推送通知管理器初始化成功');
|
||||||
return true;
|
return true;
|
||||||
@@ -313,16 +310,6 @@ export class PushNotificationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置令牌刷新监听器
|
|
||||||
*/
|
|
||||||
private setupTokenRefreshListener(): void {
|
|
||||||
// 监听令牌变化(iOS上通常不会频繁变化)
|
|
||||||
Notifications.addNotificationResponseReceivedListener((response) => {
|
|
||||||
console.log('收到推送通知响应:', response);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前设备令牌
|
* 获取当前设备令牌
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type {
|
|||||||
MedicationStatus,
|
MedicationStatus,
|
||||||
} from '@/types/medication';
|
} from '@/types/medication';
|
||||||
import { convertMedicationDataToWidget, syncMedicationDataToWidget } from '@/utils/widgetDataSync';
|
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 dayjs from 'dayjs';
|
||||||
import type { RootState } from './index';
|
import type { RootState } from './index';
|
||||||
|
|
||||||
@@ -695,6 +695,9 @@ export const {
|
|||||||
|
|
||||||
// ==================== Selectors ====================
|
// ==================== Selectors ====================
|
||||||
|
|
||||||
|
// 空数组常量,避免每次都创建新数组
|
||||||
|
const EMPTY_RECORDS_ARRAY: MedicationRecord[] = [];
|
||||||
|
|
||||||
export const selectMedicationsState = (state: RootState) => state.medications;
|
export const selectMedicationsState = (state: RootState) => state.medications;
|
||||||
export const selectMedications = (state: RootState) => state.medications.medications;
|
export const selectMedications = (state: RootState) => state.medications.medications;
|
||||||
export const selectActiveMedications = (state: RootState) =>
|
export const selectActiveMedications = (state: RootState) =>
|
||||||
@@ -708,7 +711,7 @@ export const selectOverallStats = (state: RootState) => state.medications.overal
|
|||||||
* 获取指定日期的服药记录
|
* 获取指定日期的服药记录
|
||||||
*/
|
*/
|
||||||
export const selectMedicationRecordsByDate = (date: string) => (state: RootState) => {
|
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) => {
|
export const selectSelectedDateMedicationRecords = (state: RootState) => {
|
||||||
const selectedDate = state.medications.selectedDate;
|
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)
|
* 排序规则:优先显示未服用的药品(upcoming、missed),然后是已服用的药品(taken、skipped)
|
||||||
*/
|
*/
|
||||||
export const selectMedicationDisplayItemsByDate = (date: string) => (state: RootState) => {
|
export const selectMedicationDisplayItemsByDate = (date: string) =>
|
||||||
const records = state.medications.medicationRecords[date] || [];
|
createSelector(
|
||||||
const medications = state.medications.medications;
|
[
|
||||||
|
(state: RootState) => state.medications.medicationRecords[date] || EMPTY_RECORDS_ARRAY,
|
||||||
// 创建药物ID到药物的映射
|
(state: RootState) => state.medications.medications,
|
||||||
const medicationMap = new Map<string, Medication>();
|
],
|
||||||
medications.forEach((med) => medicationMap.set(med.id, med));
|
(records, medications) => {
|
||||||
|
// 创建药物ID到药物的映射
|
||||||
// 转换为展示项
|
const medicationMap = new Map<string, Medication>();
|
||||||
const displayItems = records
|
medications.forEach((med) => medicationMap.set(med.id, med));
|
||||||
.map((record) => {
|
|
||||||
const medication = medicationMap.get(record.medicationId);
|
|
||||||
if (!medication) return null;
|
|
||||||
|
|
||||||
// 格式化剂量
|
// 转换为展示项
|
||||||
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格式)
|
// 排序:未服用的药品(upcoming、missed)优先,已服用的药品(taken、skipped)其次
|
||||||
// 服务端返回的是UTC时间,需要转换为用户本地时间显示
|
// 在同一组内,按计划时间升序排列
|
||||||
const localTime = dayjs(record.scheduledTime).format('HH:mm');
|
return displayItems.sort((a, b) => {
|
||||||
const scheduledTime = localTime || '00:00';
|
// 定义状态优先级:数值越小优先级越高
|
||||||
|
const statusPriority: Record<MedicationStatus, number> = {
|
||||||
// 频率描述
|
'missed': 1, // 已错过 - 最高优先级
|
||||||
const frequency = medication.repeatPattern === 'daily' ? '每日' : '自定义';
|
'upcoming': 2, // 待服用
|
||||||
|
'taken': 3, // 已服用
|
||||||
return {
|
'skipped': 4, // 已跳过
|
||||||
id: record.id,
|
};
|
||||||
name: medication.name,
|
|
||||||
dosage,
|
const priorityA = statusPriority[a.status];
|
||||||
scheduledTime,
|
const priorityB = statusPriority[b.status];
|
||||||
frequency,
|
|
||||||
status: record.status,
|
// 首先按状态优先级排序
|
||||||
recordId: record.id,
|
if (priorityA !== priorityB) {
|
||||||
medicationId: medication.id,
|
return priorityA - priorityB;
|
||||||
image: medication.photoUrl ? { uri: medication.photoUrl } : undefined
|
}
|
||||||
} as import('@/types/medication').MedicationDisplayItem;
|
|
||||||
})
|
// 状态相同时,按计划时间升序排列
|
||||||
.filter((item): item is import('@/types/medication').MedicationDisplayItem => item !== null);
|
return a.scheduledTime.localeCompare(b.scheduledTime);
|
||||||
|
});
|
||||||
// 排序:未服用的药品(upcoming、missed)优先,已服用的药品(taken、skipped)其次
|
|
||||||
// 在同一组内,按计划时间升序排列
|
|
||||||
return displayItems.sort((a, b) => {
|
|
||||||
// 定义状态优先级:数值越小优先级越高
|
|
||||||
const statusPriority: Record<MedicationStatus, number> = {
|
|
||||||
'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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== Export ====================
|
// ==================== Export ====================
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import AsyncStorage from '@/utils/kvStore';
|
import AsyncStorage from '@/utils/kvStore';
|
||||||
import { logger } from '@/utils/logger';
|
import { logger } from '@/utils/logger';
|
||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import type { RootState } from './index';
|
import type { RootState } from './index';
|
||||||
|
|
||||||
// Tab 配置接口
|
// Tab 配置接口
|
||||||
@@ -190,10 +190,15 @@ export const {
|
|||||||
|
|
||||||
// Selectors
|
// Selectors
|
||||||
export const selectTabBarConfigs = (state: RootState) => state.tabBarConfig.configs;
|
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)
|
.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;
|
export const selectIsInitialized = (state: RootState) => state.tabBarConfig.isInitialized;
|
||||||
|
|
||||||
// 按 id 获取配置
|
// 按 id 获取配置
|
||||||
|
|||||||
Reference in New Issue
Block a user