feat: 集成推送通知功能及相关组件
- 在项目中引入expo-notifications库,支持本地推送通知功能 - 实现通知权限管理,用户可选择开启或关闭通知 - 新增通知发送、定时通知和重复通知功能 - 更新个人页面,集成通知开关和权限请求逻辑 - 编写推送通知功能实现文档,详细描述功能和使用方法 - 优化心情日历页面,确保数据实时刷新
This commit is contained in:
@@ -1,114 +0,0 @@
|
||||
import { Platform } from 'react-native';
|
||||
import {
|
||||
HKQuantityTypeIdentifier,
|
||||
HKQuantitySample,
|
||||
getMostRecentQuantitySample,
|
||||
isAvailable,
|
||||
authorize,
|
||||
} from 'react-native-health';
|
||||
|
||||
interface HealthData {
|
||||
oxygenSaturation: number | null;
|
||||
heartRate: number | null;
|
||||
lastUpdated: Date | null;
|
||||
}
|
||||
|
||||
class HealthDataService {
|
||||
private static instance: HealthDataService;
|
||||
private isAuthorized = false;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): HealthDataService {
|
||||
if (!HealthDataService.instance) {
|
||||
HealthDataService.instance = new HealthDataService();
|
||||
}
|
||||
return HealthDataService.instance;
|
||||
}
|
||||
|
||||
async requestAuthorization(): Promise<boolean> {
|
||||
if (Platform.OS !== 'ios') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const available = await isAvailable();
|
||||
if (!available) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const permissions = [
|
||||
{
|
||||
type: HKQuantityTypeIdentifier.OxygenSaturation,
|
||||
access: 'read' as const
|
||||
},
|
||||
{
|
||||
type: HKQuantityTypeIdentifier.HeartRate,
|
||||
access: 'read' as const
|
||||
}
|
||||
];
|
||||
|
||||
const authorized = await authorize(permissions);
|
||||
this.isAuthorized = authorized;
|
||||
return authorized;
|
||||
} catch (error) {
|
||||
console.error('Health data authorization error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getOxygenSaturation(): Promise<number | null> {
|
||||
if (!this.isAuthorized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const sample: HKQuantitySample | null = await getMostRecentQuantitySample(
|
||||
HKQuantityTypeIdentifier.OxygenSaturation
|
||||
);
|
||||
|
||||
if (sample) {
|
||||
return Number(sample.value.toFixed(1));
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error reading oxygen saturation:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getHeartRate(): Promise<number | null> {
|
||||
if (!this.isAuthorized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const sample: HKQuantitySample | null = await getMostRecentQuantitySample(
|
||||
HKQuantityTypeIdentifier.HeartRate
|
||||
);
|
||||
|
||||
if (sample) {
|
||||
return Math.round(sample.value);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error reading heart rate:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getHealthData(): Promise<HealthData> {
|
||||
const [oxygenSaturation, heartRate] = await Promise.all([
|
||||
this.getOxygenSaturation(),
|
||||
this.getHeartRate()
|
||||
]);
|
||||
|
||||
return {
|
||||
oxygenSaturation,
|
||||
heartRate,
|
||||
lastUpdated: new Date()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default HealthDataService.getInstance();
|
||||
352
services/notifications.ts
Normal file
352
services/notifications.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
// 配置通知处理方式
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldShowAlert: true,
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: true,
|
||||
shouldShowBanner: true,
|
||||
shouldShowList: true,
|
||||
}),
|
||||
});
|
||||
|
||||
export interface NotificationData {
|
||||
title: string;
|
||||
body: string;
|
||||
data?: Record<string, any>;
|
||||
sound?: boolean;
|
||||
priority?: 'default' | 'normal' | 'high';
|
||||
vibrate?: number[];
|
||||
}
|
||||
|
||||
export class NotificationService {
|
||||
private static instance: NotificationService;
|
||||
private isInitialized = false;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): NotificationService {
|
||||
if (!NotificationService.instance) {
|
||||
NotificationService.instance = new NotificationService();
|
||||
}
|
||||
return NotificationService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化推送通知服务
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
try {
|
||||
// 请求通知权限
|
||||
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
||||
let finalStatus = existingStatus;
|
||||
|
||||
if (existingStatus !== 'granted') {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
finalStatus = status;
|
||||
}
|
||||
|
||||
if (finalStatus !== 'granted') {
|
||||
console.warn('推送通知权限未授予');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取推送令牌(用于远程推送,本地推送不需要)
|
||||
if (Platform.OS !== 'web') {
|
||||
const token = await Notifications.getExpoPushTokenAsync({
|
||||
projectId: 'your-project-id', // 需要替换为实际的Expo项目ID
|
||||
});
|
||||
console.log('推送令牌:', token.data);
|
||||
}
|
||||
|
||||
// 设置通知监听器
|
||||
this.setupNotificationListeners();
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('推送通知服务初始化成功');
|
||||
} catch (error) {
|
||||
console.error('推送通知服务初始化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置通知监听器
|
||||
*/
|
||||
private setupNotificationListeners(): void {
|
||||
// 监听通知接收
|
||||
Notifications.addNotificationReceivedListener((notification) => {
|
||||
console.log('收到通知:', notification);
|
||||
});
|
||||
|
||||
// 监听通知点击
|
||||
Notifications.addNotificationResponseReceivedListener((response) => {
|
||||
console.log('用户点击了通知:', response);
|
||||
// 这里可以处理通知点击后的逻辑
|
||||
this.handleNotificationResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理通知响应
|
||||
*/
|
||||
private handleNotificationResponse(response: Notifications.NotificationResponse): void {
|
||||
const { notification } = response;
|
||||
const data = notification.request.content.data;
|
||||
|
||||
// 根据通知类型处理不同的逻辑
|
||||
if (data?.type === 'workout_reminder') {
|
||||
// 处理运动提醒
|
||||
console.log('用户点击了运动提醒通知');
|
||||
} else if (data?.type === 'goal_achievement') {
|
||||
// 处理目标达成通知
|
||||
console.log('用户点击了目标达成通知');
|
||||
} else if (data?.type === 'mood_checkin') {
|
||||
// 处理心情打卡提醒
|
||||
console.log('用户点击了心情打卡提醒');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送本地推送通知
|
||||
*/
|
||||
async scheduleLocalNotification(
|
||||
notification: NotificationData,
|
||||
trigger?: Notifications.NotificationTriggerInput
|
||||
): Promise<string> {
|
||||
try {
|
||||
const notificationId = await Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: notification.title,
|
||||
body: notification.body,
|
||||
data: notification.data || {},
|
||||
sound: notification.sound ? 'default' : undefined,
|
||||
priority: notification.priority || 'default',
|
||||
vibrate: notification.vibrate,
|
||||
},
|
||||
trigger: trigger || null, // null表示立即发送
|
||||
});
|
||||
|
||||
console.log('本地通知已安排,ID:', notificationId);
|
||||
return notificationId;
|
||||
} catch (error) {
|
||||
console.error('安排本地通知失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送立即通知
|
||||
*/
|
||||
async sendImmediateNotification(notification: NotificationData): Promise<string> {
|
||||
return this.scheduleLocalNotification(notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安排定时通知
|
||||
*/
|
||||
async scheduleNotificationAtDate(
|
||||
notification: NotificationData,
|
||||
date: Date
|
||||
): Promise<string> {
|
||||
try {
|
||||
const notificationId = await Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: notification.title,
|
||||
body: notification.body,
|
||||
data: notification.data || {},
|
||||
sound: notification.sound ? 'default' : undefined,
|
||||
priority: notification.priority || 'default',
|
||||
vibrate: notification.vibrate,
|
||||
},
|
||||
trigger: {
|
||||
date: date.getTime(),
|
||||
} as any,
|
||||
});
|
||||
|
||||
console.log('定时通知已安排,ID:', notificationId);
|
||||
return notificationId;
|
||||
} catch (error) {
|
||||
console.error('安排定时通知失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安排重复通知(仅支持秒级别)
|
||||
*/
|
||||
async scheduleRepeatingNotification(
|
||||
notification: NotificationData,
|
||||
interval: {
|
||||
seconds?: number;
|
||||
minutes?: number;
|
||||
hours?: number;
|
||||
days?: number;
|
||||
weeks?: number;
|
||||
months?: number;
|
||||
years?: number;
|
||||
}
|
||||
): Promise<string> {
|
||||
try {
|
||||
// 计算总秒数
|
||||
const totalSeconds =
|
||||
(interval.seconds || 0) +
|
||||
(interval.minutes || 0) * 60 +
|
||||
(interval.hours || 0) * 3600 +
|
||||
(interval.days || 0) * 86400 +
|
||||
(interval.weeks || 0) * 604800 +
|
||||
(interval.months || 0) * 2592000 +
|
||||
(interval.years || 0) * 31536000;
|
||||
|
||||
if (totalSeconds <= 0) {
|
||||
throw new Error('重复间隔必须大于0');
|
||||
}
|
||||
|
||||
const notificationId = await Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: notification.title,
|
||||
body: notification.body,
|
||||
data: notification.data || {},
|
||||
sound: notification.sound ? 'default' : undefined,
|
||||
priority: notification.priority || 'default',
|
||||
vibrate: notification.vibrate,
|
||||
},
|
||||
trigger: {
|
||||
seconds: totalSeconds,
|
||||
repeats: true,
|
||||
} as any,
|
||||
});
|
||||
|
||||
console.log('重复通知已安排,ID:', notificationId);
|
||||
return notificationId;
|
||||
} catch (error) {
|
||||
console.error('安排重复通知失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消特定通知
|
||||
*/
|
||||
async cancelNotification(notificationId: string): Promise<void> {
|
||||
try {
|
||||
await Notifications.cancelScheduledNotificationAsync(notificationId);
|
||||
console.log('通知已取消:', notificationId);
|
||||
} catch (error) {
|
||||
console.error('取消通知失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有通知
|
||||
*/
|
||||
async cancelAllNotifications(): Promise<void> {
|
||||
try {
|
||||
await Notifications.cancelAllScheduledNotificationsAsync();
|
||||
console.log('所有通知已取消');
|
||||
} catch (error) {
|
||||
console.error('取消所有通知失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已安排的通知
|
||||
*/
|
||||
async getAllScheduledNotifications(): Promise<Notifications.NotificationRequest[]> {
|
||||
try {
|
||||
const notifications = await Notifications.getAllScheduledNotificationsAsync();
|
||||
return notifications;
|
||||
} catch (error) {
|
||||
console.error('获取已安排通知失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知权限状态
|
||||
*/
|
||||
async getPermissionStatus(): Promise<Notifications.PermissionStatus> {
|
||||
try {
|
||||
const { status } = await Notifications.getPermissionsAsync();
|
||||
return status;
|
||||
} catch (error) {
|
||||
console.error('获取通知权限状态失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求通知权限
|
||||
*/
|
||||
async requestPermission(): Promise<Notifications.PermissionStatus> {
|
||||
try {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
return status;
|
||||
} catch (error) {
|
||||
console.error('请求通知权限失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const notificationService = NotificationService.getInstance();
|
||||
|
||||
// 预定义的推送通知类型
|
||||
export const NotificationTypes = {
|
||||
WORKOUT_REMINDER: 'workout_reminder',
|
||||
GOAL_ACHIEVEMENT: 'goal_achievement',
|
||||
MOOD_CHECKIN: 'mood_checkin',
|
||||
NUTRITION_REMINDER: 'nutrition_reminder',
|
||||
PROGRESS_UPDATE: 'progress_update',
|
||||
} as const;
|
||||
|
||||
// 便捷方法
|
||||
export const sendWorkoutReminder = (title: string, body: string, date?: Date) => {
|
||||
const notification: NotificationData = {
|
||||
title,
|
||||
body,
|
||||
data: { type: NotificationTypes.WORKOUT_REMINDER },
|
||||
sound: true,
|
||||
priority: 'high',
|
||||
};
|
||||
|
||||
if (date) {
|
||||
return notificationService.scheduleNotificationAtDate(notification, date);
|
||||
} else {
|
||||
return notificationService.sendImmediateNotification(notification);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendGoalAchievement = (title: string, body: string) => {
|
||||
const notification: NotificationData = {
|
||||
title,
|
||||
body,
|
||||
data: { type: NotificationTypes.GOAL_ACHIEVEMENT },
|
||||
sound: true,
|
||||
priority: 'high',
|
||||
};
|
||||
|
||||
return notificationService.sendImmediateNotification(notification);
|
||||
};
|
||||
|
||||
export const sendMoodCheckinReminder = (title: string, body: string, date?: Date) => {
|
||||
const notification: NotificationData = {
|
||||
title,
|
||||
body,
|
||||
data: { type: NotificationTypes.MOOD_CHECKIN },
|
||||
sound: true,
|
||||
priority: 'normal',
|
||||
};
|
||||
|
||||
if (date) {
|
||||
return notificationService.scheduleNotificationAtDate(notification, date);
|
||||
} else {
|
||||
return notificationService.sendImmediateNotification(notification);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user