feat: 集成推送通知功能及相关组件
- 在项目中引入expo-notifications库,支持本地推送通知功能 - 实现通知权限管理,用户可选择开启或关闭通知 - 新增通知发送、定时通知和重复通知功能 - 更新个人页面,集成通知开关和权限请求逻辑 - 编写推送通知功能实现文档,详细描述功能和使用方法 - 优化心情日历页面,确保数据实时刷新
This commit is contained in:
155
docs/mood-calendar-refresh-fix.md
Normal file
155
docs/mood-calendar-refresh-fix.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# 心情日历数据实时刷新修复方案
|
||||
|
||||
## 问题描述
|
||||
|
||||
用户编辑或新建心情后,当天的心情数据没有实时更新,必须退出页面或切换日期再切换回来才会生效。
|
||||
|
||||
## 问题原因
|
||||
|
||||
1. **本地状态与 Redux 状态不同步**:心情日历页面使用本地状态 `moodRecords` 和 `selectedDateMood` 来管理数据,而不是直接使用 Redux store 中的数据。
|
||||
|
||||
2. **缺少页面焦点监听**:当用户从编辑页面返回时,没有监听页面焦点变化来刷新数据。
|
||||
|
||||
3. **数据流不一致**:编辑页面通过 Redux 更新数据,但日历页面没有监听这些变化。
|
||||
|
||||
## 修复方案
|
||||
|
||||
### 1. 迁移到 Redux 状态管理
|
||||
|
||||
**修改文件**: `app/mood/calendar.tsx`
|
||||
|
||||
#### 移除本地状态
|
||||
```typescript
|
||||
// 移除这些本地状态
|
||||
const [selectedDateMood, setSelectedDateMood] = useState<any>(null);
|
||||
const [moodRecords, setMoodRecords] = useState<Record<string, any[]>>({});
|
||||
```
|
||||
|
||||
#### 使用 Redux 状态
|
||||
```typescript
|
||||
// 使用 Redux store 中的数据
|
||||
const moodRecords = useAppSelector(state => state.mood.moodRecords);
|
||||
|
||||
// 获取选中日期的数据
|
||||
const selectedDateString = selectedDay ? dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD') : null;
|
||||
const selectedDateMood = selectedDateString ? useAppSelector(selectLatestMoodRecordByDate(selectedDateString)) : null;
|
||||
```
|
||||
|
||||
### 2. 添加页面焦点监听
|
||||
|
||||
#### 导入 useFocusEffect
|
||||
```typescript
|
||||
import { router, useLocalSearchParams, useFocusEffect } from 'expo-router';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
```
|
||||
|
||||
#### 添加焦点监听器
|
||||
```typescript
|
||||
// 监听页面焦点变化,当从编辑页面返回时刷新数据
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
// 当页面获得焦点时,刷新当前月份的数据和选中日期的数据
|
||||
const refreshData = async () => {
|
||||
if (selectedDay) {
|
||||
const selectedDateString = dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD');
|
||||
await loadDailyMoodCheckins(selectedDateString);
|
||||
}
|
||||
await loadMonthMoodData(currentMonth);
|
||||
};
|
||||
refreshData();
|
||||
}, [currentMonth, selectedDay, loadDailyMoodCheckins, loadMonthMoodData])
|
||||
);
|
||||
```
|
||||
|
||||
### 3. 简化数据加载逻辑
|
||||
|
||||
#### 移除手动状态更新
|
||||
```typescript
|
||||
// 加载选中日期的心情记录
|
||||
const loadDailyMoodCheckins = async (dateString: string) => {
|
||||
try {
|
||||
await fetchMoodRecords(dateString);
|
||||
// 不需要手动设置 selectedDateMood,因为它现在从 Redux store 中获取
|
||||
} catch (error) {
|
||||
console.error('加载心情记录失败:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### 简化月份数据加载
|
||||
```typescript
|
||||
// 加载整个月份的心情数据
|
||||
const loadMonthMoodData = async (targetMonth: Date) => {
|
||||
try {
|
||||
const startDate = dayjs(targetMonth).startOf('month').format('YYYY-MM-DD');
|
||||
const endDate = dayjs(targetMonth).endOf('month').format('YYYY-MM-DD');
|
||||
|
||||
const historyData = await fetchMoodHistoryRecords({ startDate, endDate });
|
||||
|
||||
// 历史记录已经通过 fetchMoodHistoryRecords 自动存储到 Redux store 中
|
||||
// 不需要额外的处理
|
||||
} catch (error) {
|
||||
console.error('加载月份心情数据失败:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 4. 更新心情图标渲染
|
||||
|
||||
#### 使用 Redux 数据渲染图标
|
||||
```typescript
|
||||
const renderMoodIcon = (day: number | null, isSelected: boolean) => {
|
||||
if (!day) return null;
|
||||
|
||||
// 检查该日期是否有心情记录 - 现在从 Redux store 中获取
|
||||
const dayDateString = dayjs(currentMonth).date(day).format('YYYY-MM-DD');
|
||||
const dayRecords = moodRecords[dayDateString] || [];
|
||||
const moodRecord = dayRecords.length > 0 ? dayRecords[0] : null;
|
||||
|
||||
if (moodRecord) {
|
||||
const mood = moodOptions.find(m => m.type === moodRecord.moodType);
|
||||
return (
|
||||
<View style={[styles.moodIconContainer, { backgroundColor: mood?.color }]}>
|
||||
<View style={styles.moodIcon}>
|
||||
<Text style={styles.moodEmoji}>{mood?.emoji || '😊'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.defaultMoodIcon}>
|
||||
<Text style={styles.defaultMoodEmoji}>😊</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 修复效果
|
||||
|
||||
### 修复前
|
||||
- 用户编辑心情后,日历页面数据不更新
|
||||
- 需要退出页面或切换日期才能看到变化
|
||||
- 用户体验不佳
|
||||
|
||||
### 修复后
|
||||
- 用户编辑心情后,日历页面数据立即更新
|
||||
- 页面焦点变化时自动刷新数据
|
||||
- 数据状态统一管理,避免不一致
|
||||
|
||||
## 技术要点
|
||||
|
||||
1. **统一状态管理**:所有心情数据都通过 Redux 管理,确保数据一致性。
|
||||
|
||||
2. **响应式更新**:使用 `useAppSelector` 监听 Redux 状态变化,自动更新 UI。
|
||||
|
||||
3. **页面焦点监听**:使用 `useFocusEffect` 监听页面焦点变化,确保从其他页面返回时数据是最新的。
|
||||
|
||||
4. **异步数据加载**:保持异步数据加载的性能优势,同时确保数据同步。
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `app/mood/calendar.tsx` - 心情日历页面
|
||||
- `app/mood/edit.tsx` - 心情编辑页面
|
||||
- `store/moodSlice.ts` - 心情 Redux slice
|
||||
- `hooks/useMoodData.ts` - 心情数据管理 hook
|
||||
304
docs/notification-implementation.md
Normal file
304
docs/notification-implementation.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# 推送通知功能实现文档
|
||||
|
||||
## 概述
|
||||
|
||||
本项目已成功集成本地推送通知功能,使用 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
|
||||
});
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
本推送通知功能实现完整、功能丰富,支持多种通知类型和场景。通过合理的架构设计和错误处理,确保了功能的稳定性和用户体验。开发者可以根据具体需求灵活使用各种通知功能,为用户提供个性化的提醒服务。
|
||||
Reference in New Issue
Block a user